Repository: canonical/snapcraft

Issue:
LXD errors from Craft Providers not properly presented
### Bug Description

When there's a Craft Providers error related to running LXD, as a user we get a traceback

### To Reproduce

On a fresh system:

- install snapcraft
- run a core22 snapcraft project

You will be queried to install LXD, and that works well, but we are later presented with an error not being setup.

### Environment

Fresh Ubuntu 22.04.2 LTS

### snapcraft.yaml

```shell
name: foo
version: irrelevant
summary: reproducer
description: will only trigger if LXD has not been configured to work correctly
base: core22

parts:
    foo:
        plugin: nil
```


### Relevant log output

```shell
sergiusens@imladris:~/projects/snaps/darktable$ snapcraft 
LXD is required but not installed. Do you wish to install LXD and configure it with the defaults? [y/N]: y
[sudo] password for sergiusens: 
lxd 5.15-002fa0f from Canonical✓ installed
WARNING: There is 1 new warning. See 'snap warnings'.
Traceback (most recent call last):
  File "/snap/snapcraft/9362/bin/snapcraft", line 8, in <module>
    sys.exit(run())
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/cli.py", line 255, in run
    _run_dispatcher(dispatcher, global_args)
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/cli.py", line 228, in _run_dispatcher
    dispatcher.run()
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/craft_cli/dispatcher.py", line 448, in run
    return self._loaded_command.run(self._parsed_command_args)
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/commands/lifecycle.py", line 265, in run
    super().run(parsed_args)
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/commands/lifecycle.py", line 138, in run
    parts_lifecycle.run(self.name, parsed_args)
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/parts/lifecycle.py", line 216, in run
    _run_command(
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/parts/lifecycle.py", line 262, in _run_command
    _run_in_provider(project, command_name, parsed_args)
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/parts/lifecycle.py", line 487, in _run_in_provider
    providers.ensure_provider_is_available(provider)
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/snapcraft/providers.py", line 148, in ensure_provider_is_available
    LXDProvider.ensure_provider_is_available()
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/craft_providers/lxd/lxd_provider.py", line 69, in ensure_provider_is_available
    install()
  File "/snap/snapcraft/9362/lib/python3.8/site-packages/craft_providers/lxd/installer.py", line 71, in install
    raise errors.LXDInstallationError(
craft_providers.lxd.errors.LXDInstallationError: Failed to install LXD: user must be manually added to 'lxd' group before using LXD.
Visit https://linuxcontainers.org/lxd/getting-started-cli/ for instructions on installing and configuring LXD for your operating system.
```


### Additional context

_No response_


Discussion:

Resolving patch:
diff --git a/snapcraft/cli.py b/snapcraft/cli.py
--- a/snapcraft/cli.py
+++ b/snapcraft/cli.py
@@ -1,6 +1,6 @@
 # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
 #
-# Copyright 2022 Canonical Ltd.
+# Copyright 2022-2023 Canonical Ltd.
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 3 as
@@ -26,6 +26,7 @@
 import craft_cli
 import craft_store
 from craft_cli import ArgumentParsingError, EmitterMode, ProvideHelpException, emit
+from craft_providers import ProviderError
 
 import snapcraft
 import snapcraft_legacy
@@ -290,6 +291,9 @@ def run():  # noqa: C901
     except craft_store.errors.CraftStoreError as err:
         _emit_error(craft_cli.errors.CraftError(f"craft-store error: {err}"))
         retcode = 1
+    except ProviderError as err:
+        _emit_error(craft_cli.errors.CraftError(f"craft-providers error: {err}"))
+        retcode = 1
     except errors.LinterError as err:
         emit.error(craft_cli.errors.CraftError(f"linter error: {err}"))
         retcode = err.exit_code


Test patch:
diff --git a/tests/unit/cli/test_exit.py b/tests/unit/cli/test_exit.py
--- a/tests/unit/cli/test_exit.py
+++ b/tests/unit/cli/test_exit.py
@@ -1,6 +1,6 @@
 # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
 #
-# Copyright 2022 Canonical Ltd.
+# Copyright 2022-2023 Canonical Ltd.
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 3 as
@@ -21,6 +21,7 @@
 import craft_store.errors
 import pytest
 from craft_cli import CraftError
+from craft_providers import ProviderError
 
 from snapcraft import cli
 
@@ -50,6 +51,29 @@ def test_no_keyring_error(capsys, mocker):
     )
 
 
+def test_craft_providers_error(capsys, mocker):
+    """Catch craft-providers errors."""
+    mocker.patch.object(sys, "argv", ["cmd", "pull"])
+    mocker.patch.object(sys.stdin, "isatty", return_value=True)
+    mocker.patch(
+        "snapcraft.commands.lifecycle.PullCommand.run",
+        side_effect=ProviderError(
+            brief="test brief",
+            details="test details",
+            resolution="test resolution",
+        ),
+    )
+
+    cli.run()
+
+    stderr = capsys.readouterr().err.splitlines()
+
+    # Simple verification that our expected message is being printed
+    assert stderr[0].startswith("craft-providers error: test brief")
+    assert stderr[1].startswith("test details")
+    assert stderr[2].startswith("test resolution")
+
+
 @pytest.mark.parametrize("is_managed,report_errors", [(True, False), (False, True)])
 def test_emit_error(emitter, mocker, is_managed, report_errors):
     mocker.patch("snapcraft.utils.is_managed_mode", return_value=is_managed)