Repository: canonical/charmcraft

Issue:
Stop "internal Charmcraft" leaking log in the container
When running `pack` in normal mode (using a container, not destructive mode), and it ends in error, the Charmcraft process run inside the container exposes the problem and where the log is. And it shouldn't expose the internal log.

For example, this is a complete output (line numbers added for reference):

```
0   $ charmcraft pack
1   Packing the charm
2   Parts processing error: Failed to run the build script for part 'charm'.
3   Full execution log: '/tmp/charmcraft.log'
4   Failed to build charm for bases index '0'.
5   Full execution log: '/home/test/.cache/charmcraft/log/charmcraft-20220418-121745.068589.log'
```

The lines 2 and 3 are shown by the internal Charmcraft. While the error indication (line 2) may be valuable, line 3 should never appear.

Discussion:

Patch:
diff --git a/charmcraft/main.py b/charmcraft/main.py
index 49987e9c..70ef3b82 100644
--- a/charmcraft/main.py
+++ b/charmcraft/main.py
@@ -116,6 +116,20 @@ def _get_system_details():
     return f"System details: {os_platform}; Environment: {env_string}"
 
 
+def _emit_error(error, cause=None):
+    """Emit the error in a centralized way so we can alter it consistently."""
+    # set the cause, if any
+    if cause is not None:
+        error.__cause__ = cause
+
+    # if it's a charmcraft running inside a provided instance, do not report the internal logpath
+    if env.is_charmcraft_running_in_managed_mode():
+        error.logpath_report = False
+
+    # finally, emit
+    emit.error(error)
+
+
 def main(argv=None):
     """Provide the main entry point."""
     if env.is_charmcraft_running_in_managed_mode():
@@ -175,21 +189,19 @@ def main(argv=None):
         emit.ended_ok()
         retcode = 0
     except CraftError as err:
-        emit.error(err)
+        _emit_error(err)
         retcode = err.retcode
     except errors.CraftStoreError as err:
         error = CraftError(f"craft-store error: {err}")
-        emit.error(error)
+        _emit_error(error)
         retcode = 1
     except KeyboardInterrupt as exc:
         error = CraftError("Interrupted.")
-        error.__cause__ = exc
-        emit.error(error)
+        _emit_error(error, cause=exc)
         retcode = 1
     except Exception as err:
         error = CraftError(f"charmcraft internal error: {err!r}")
-        error.__cause__ = err
-        emit.error(error)
+        _emit_error(error, cause=err)
         retcode = 1
     else:
         emit.ended_ok()
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 6ec9369c..8c169bff 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -5,7 +5,7 @@ cffi==1.15.0
 charset-normalizer==2.0.12
 click==8.1.3
 coverage==6.4
-craft-cli==0.6.0
+craft-cli==0.7.0
 craft-parts==1.7.1
 craft-providers==1.3.0
 craft-store==2.1.1
diff --git a/requirements.txt b/requirements.txt
index cd081c40..4537d372 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@ attrs==21.4.0
 certifi==2022.5.18.1
 cffi==1.15.0
 charset-normalizer==2.0.12
-craft-cli==0.6.0
+craft-cli==0.7.0
 craft-parts==1.7.1
 craft-providers==1.3.0
 craft-store==2.1.1


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 8758e8bf..af3e500a 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -29,6 +29,7 @@ from craft_cli import (
     EmitterMode,
     ProvideHelpException,
 )
+from craft_store.errors import CraftStoreError
 
 from charmcraft import __version__, env, utils
 from charmcraft.cmdbase import BaseCommand, JSON_FORMAT
@@ -75,7 +76,7 @@ def test_main_ok():
     )
 
 
-def test_main_managed_instance(monkeypatch):
+def test_main_managed_instance_init(monkeypatch):
     """Init emitter with a specific log filepath."""
     monkeypatch.setenv("CHARMCRAFT_MANAGED_MODE", "1")
 
@@ -93,6 +94,30 @@ def test_main_managed_instance(monkeypatch):
     )
 
 
+@pytest.mark.parametrize(
+    "side_effect",
+    [
+        ValueError("broken"),
+        KeyboardInterrupt("interrupted"),
+        CraftStoreError("bad server"),
+        CraftError("bad charmcraft"),
+    ],
+)
+def test_main_managed_instance_error(monkeypatch, side_effect, config):
+    """The managed instance will not expose the "internal" log filepath."""
+    monkeypatch.setenv("CHARMCRAFT_MANAGED_MODE", "1")
+
+    with patch("charmcraft.main.emit") as emit_mock:
+        with patch("charmcraft.main.Dispatcher.pre_parse_args") as d_mock:
+            d_mock.side_effect = side_effect
+            main(["charmcraft", "version"])
+
+    # check that the error sent to Craft CLI will not report the logpath
+    (end_in_error_call,) = emit_mock.error.mock_calls
+    error = end_in_error_call.args[0]
+    assert error.logpath_report is False
+
+
 def test_main_load_config_ok(create_config):
     """Command is properly executed, after loading and receiving the config."""
     tmp_path = create_config(