feat: add prune instances#1058
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements the new prune-instances CLI command requested in #991, wiring it through ProviderService so users can prune managed provider instances (optionally including base/template instances).
Changes:
- Add
PruneInstancesCommandand register it in the “Other” command group and command exports. - Add
ProviderService.prune_instances()to prune instances for a selected provider or across providers. - Add unit tests for the new command and service behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
craft_application/services/provider.py |
Adds prune_instances() implementation to call into craft-providers’ prune functionality. |
craft_application/commands/prune_instances.py |
Introduces the new prune-instances command and CLI flags. |
craft_application/commands/other.py |
Registers the new command in the “Other” command group. |
craft_application/commands/__init__.py |
Exports PruneInstancesCommand. |
tests/unit/services/test_provider.py |
Adds unit tests for provider-service pruning paths. |
tests/unit/commands/test_prune_instances.py |
Adds unit test for command-to-service wiring. |
tests/unit/commands/test_other.py |
Updates “Other commands” group test to include the new command. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| parsed_args = argparse.Namespace( | ||
| all_providers=True, | ||
| provider="lxd", | ||
| prune_templates=True, |
| def test_prune_instances_all_providers(monkeypatch, provider_service): | ||
| """Prune for all supported providers.""" | ||
| mock_lxd = mock.MagicMock() | ||
| mock_lxd.name = "lxd" | ||
| mock_multipass = mock.MagicMock() | ||
| mock_multipass.name = "multipass" | ||
|
|
||
| def mock_get_by_name(name): | ||
| if name == "lxd": | ||
| return mock_lxd | ||
| if name == "multipass": | ||
| return mock_multipass | ||
| raise RuntimeError(f"Unknown provider: {name}") | ||
|
|
||
| monkeypatch.setattr(provider_service, "_get_provider_by_name", mock_get_by_name) | ||
|
|
||
| provider_service.prune_instances(all_providers=True, prune_templates=False) | ||
|
|
||
| mock_lxd.prune.assert_called_once_with(prune_templates=False) | ||
| mock_multipass.prune.assert_called_once_with(prune_templates=False) |
There was a problem hiding this comment.
I don't think we need to care about this - we're able to assume commands.prune_instances is only providing lowercase provider names to this function.
| providers.append(self._get_provider_by_name("LXD")) | ||
| except RuntimeError: | ||
| emit.debug("LXD provider not available, skipping.") | ||
|
|
||
| try: | ||
| providers.append(self._get_provider_by_name("multipass")) | ||
| providers.append(self._get_provider_by_name("Multipass")) |
There was a problem hiding this comment.
Agreed, _get_provider_by_name runs lower().strip(), so you can just call it once.
| try: | ||
| providers.append(self._get_provider_by_name("lxd")) | ||
| providers.append(self._get_provider_by_name("LXD")) | ||
| except RuntimeError: | ||
| emit.debug("LXD provider not available, skipping.") | ||
|
|
||
| try: | ||
| providers.append(self._get_provider_by_name("multipass")) | ||
| providers.append(self._get_provider_by_name("Multipass")) | ||
| except RuntimeError: | ||
| emit.debug("Multipass provider not available, skipping.") | ||
|
|
There was a problem hiding this comment.
The reasoning here is correct, if you decide to do _add_provider_if_available, I make it a class level function rather than a function within a function.
| """Prune instances for the active provider.""" | ||
|
|
||
| name = "prune-instances" | ||
| help_msg = "Prune instances for the active provider" | ||
| overview = textwrap.dedent( | ||
| """ | ||
| Prune instances for the active provider. | ||
| """ | ||
| ) | ||
|
|
||
| def _fill_parser(self, parser: argparse.ArgumentParser) -> None: | ||
| parser.add_argument( | ||
| "--all-providers", | ||
| action="store_true", | ||
| help="Prune instances from all providers", | ||
| ) | ||
| parser.add_argument( | ||
| "--provider", | ||
| help="Prune for a specific provider", |
mr-cal
left a comment
There was a problem hiding this comment.
Nice start, I left most of my feedback on top of the copilot review.
| action="store_true", | ||
| help="Prune instances from all providers", | ||
| ) | ||
| parser.add_argument( |
There was a problem hiding this comment.
Can you add choices? That will improve the UX by giving quick errors up front if a user provides something besides lxd or multipass.
It will also make your life easier in the prune_instances command, because you'll know the user declared a valid provider name.
|
@copilot create a child PR that fixes the linting issues |
There was a problem hiding this comment.
Pull request overview
Adds a new prune-instances CLI command and supporting service logic to remove managed provider instances (optionally across providers), plus accompanying unit and spread coverage to exercise the behavior end-to-end (per #991).
Changes:
- Introduce
PruneInstancesCommandand register it under the “Other” command group. - Add
ProviderService.prune_instances()(with helper to skip unavailable providers) and unit tests for provider pruning behavior. - Add a spread test scenario for
testcraft prune-instances.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
craft_application/services/provider.py |
Adds prune_instances() service API and availability-aware provider selection helper. |
craft_application/commands/prune_instances.py |
Implements the new prune-instances CLI command and argument parsing. |
craft_application/commands/other.py |
Registers PruneInstancesCommand in the “Other” command group. |
craft_application/commands/__init__.py |
Exports PruneInstancesCommand from the commands package. |
tests/unit/services/test_provider.py |
Adds unit tests covering provider-service pruning scenarios. |
tests/unit/commands/test_prune_instances.py |
Adds a unit test ensuring the command calls the provider service. |
tests/unit/commands/test_other.py |
Updates command-group expectations to include the new command. |
tests/spread/testcraft/prune-instances/testcraft.yaml |
Adds a minimal testcraft project used by the spread scenario. |
tests/spread/testcraft/prune-instances/task.yaml |
Adds a spread task that creates an LXD instance then prunes it. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| providers: list[craft_providers.Provider] = [] | ||
|
|
||
| if provider_name: | ||
| providers.append(self.get_provider(name=provider_name)) |
There was a problem hiding this comment.
Agreed. Nice to see copilot is making the same suggestions I was going to make.
| def run(self, parsed_args: argparse.Namespace) -> None: | ||
| """Run the prune-instances command.""" | ||
| self._services.provider.prune_instances( | ||
| all_providers=parsed_args.all_providers, | ||
| provider_name=parsed_args.provider, | ||
| prune_templates=parsed_args.prune_templates, | ||
| ) |
There was a problem hiding this comment.
Agreed, I think include_templates accidentally got removed when making the other args mutually exclusive.
There was a problem hiding this comment.
Hmm, would the _fill_parser() method add the include_templates or prune_templates flag?
| parsed_args = argparse.Namespace( | ||
| all_providers=True, | ||
| provider="lxd", | ||
| prune_templates=True, | ||
| ) |
There was a problem hiding this comment.
Agreed - these argparse values don't reflect what a user would be able to provide at the command line.
IIRC, manually defining parsed_args = argparse.Namespace(...) in the test bypasses code like the mutually exclusive args. To test that code, you have to mock argv and assert the error with capsys (example).
I think you can do this in 2 tests. One capsys-based test that ensures the args are mutually exclusive and another test that verifies the namespace args are successfully passed to provider.prune_instances(...).
You already have the second test written, but I recommend adding parametrizing it with @pytest.mark.parameterize() such that the test ensures what you set in parsed_args = argparse.Namespace(...) actually get passed to provider.prune_instances(...).
|
|
||
| mock_lxd.prune.assert_called_once_with(prune_templates=True) | ||
|
|
||
|
|
There was a problem hiding this comment.
I think you can ignore this, as this test needs to be updated once https://github.com/canonical/craft-application/pull/1058/changes#r3120374251 is accomdated.
| fi | ||
|
|
||
| restore: | | ||
| testcraft prune-instances --all |
There was a problem hiding this comment.
Here too:
| testcraft prune-instances --all | |
| testcraft prune-instances --all-providers |
| def run(self, parsed_args: argparse.Namespace) -> None: | ||
| """Run the prune-instances command.""" | ||
| self._services.provider.prune_instances( | ||
| all_providers=parsed_args.all_providers, | ||
| provider_name=parsed_args.provider, | ||
| prune_templates=parsed_args.prune_templates, | ||
| ) |
There was a problem hiding this comment.
Agreed, I think include_templates accidentally got removed when making the other args mutually exclusive.
| providers: list[craft_providers.Provider] = [] | ||
|
|
||
| if provider_name: | ||
| providers.append(self.get_provider(name=provider_name)) |
There was a problem hiding this comment.
Agreed. Nice to see copilot is making the same suggestions I was going to make.
| parsed_args = argparse.Namespace( | ||
| all_providers=True, | ||
| provider="lxd", | ||
| prune_templates=True, |
| parsed_args = argparse.Namespace( | ||
| all_providers=True, | ||
| provider="lxd", | ||
| prune_templates=True, | ||
| ) |
There was a problem hiding this comment.
Agreed - these argparse values don't reflect what a user would be able to provide at the command line.
IIRC, manually defining parsed_args = argparse.Namespace(...) in the test bypasses code like the mutually exclusive args. To test that code, you have to mock argv and assert the error with capsys (example).
I think you can do this in 2 tests. One capsys-based test that ensures the args are mutually exclusive and another test that verifies the namespace args are successfully passed to provider.prune_instances(...).
You already have the second test written, but I recommend adding parametrizing it with @pytest.mark.parameterize() such that the test ensures what you set in parsed_args = argparse.Namespace(...) actually get passed to provider.prune_instances(...).
| def test_prune_instances_all_providers(monkeypatch, provider_service): | ||
| """Prune for all supported providers.""" | ||
| mock_lxd = mock.MagicMock() | ||
| mock_lxd.name = "lxd" | ||
| mock_multipass = mock.MagicMock() | ||
| mock_multipass.name = "multipass" | ||
|
|
||
| def mock_get_by_name(name): | ||
| if name == "lxd": | ||
| return mock_lxd | ||
| if name == "multipass": | ||
| return mock_multipass | ||
| raise RuntimeError(f"Unknown provider: {name}") | ||
|
|
||
| monkeypatch.setattr(provider_service, "_get_provider_by_name", mock_get_by_name) | ||
|
|
||
| provider_service.prune_instances(all_providers=True, prune_templates=False) | ||
|
|
||
| mock_lxd.prune.assert_called_once_with(prune_templates=False) | ||
| mock_multipass.prune.assert_called_once_with(prune_templates=False) |
There was a problem hiding this comment.
I don't think we need to care about this - we're able to assume commands.prune_instances is only providing lowercase provider names to this function.
|
|
||
| mock_lxd.prune.assert_called_once_with(prune_templates=True) | ||
|
|
||
|
|
There was a problem hiding this comment.
I think you can ignore this, as this test needs to be updated once https://github.com/canonical/craft-application/pull/1058/changes#r3120374251 is accomdated.
There was a problem hiding this comment.
Can you add a third unit test for when provider_name = None? It should call get_provider(), which you can mock to return lxd.
Co-authored-by: Callahan Kovacs <callahan.kovacs@canonical.com> Signed-off-by: Dylan <143847987+Aeonoi@users.noreply.github.com>
…tion into add-prune-instances
|
How would I go about testing for the |
This worked, thanks. |
|
Great! By the way, there is now a 3.6.0 release of craft-providers. https://canonical-craft-providers.readthedocs-hosted.com/en/latest/reference/changelog/ |
make lint && make test?docs/reference/changelog.rst)?closes #991