launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #28194
[Merge] ~lgp171188/lpcraft:add-clean-command-clean-flag into lpcraft:main
Guruprasad has proposed merging ~lgp171188/lpcraft:add-clean-command-clean-flag into lpcraft:main.
Commit message:
Implement the clean command and the clean flag to the run commands
With this, the managed environments can either be cleaned up manually
or after a run/run-once command.
LP: #1962238
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1962238 in lpcraft: "lpcraft does not remove zfs storage artifacts"
https://bugs.launchpad.net/lpcraft/+bug/1962238
For more details, see:
https://code.launchpad.net/~lgp171188/lpcraft/+git/lpcraft/+merge/416656
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/lpcraft:add-clean-command-clean-flag into lpcraft:main.
diff --git a/lpcraft/commands/clean.py b/lpcraft/commands/clean.py
new file mode 100644
index 0000000..d92dd32
--- /dev/null
+++ b/lpcraft/commands/clean.py
@@ -0,0 +1,32 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU General Public License version 3 (see the file LICENSE).
+
+from argparse import Namespace
+from pathlib import Path
+
+from craft_cli import emit
+
+from lpcraft.config import Config
+from lpcraft.providers import get_provider
+
+
+def clean(args: Namespace) -> int:
+ """
+ Clean the managed environments for a project.
+ """
+ # Verify that the there is an lpcraft configuration file
+ # before trying to clean a project.
+ config_path = getattr(args, "config", Path(".launchpad.yaml"))
+ _ = Config.load(config_path)
+
+ cwd = Path.cwd()
+ emit.progress(f"Cleaning project {cwd.name!r}.")
+
+ provider = get_provider()
+ provider.ensure_provider_is_available()
+
+ provider.clean_project_environments(
+ project_name=cwd.name, project_path=cwd
+ )
+ emit.message(f"Cleaned project {cwd.name!r}.")
+ return 0
diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py
index 950549b..1850dce 100644
--- a/lpcraft/commands/run.py
+++ b/lpcraft/commands/run.py
@@ -17,6 +17,7 @@ from craft_providers.actions.snap_installer import install_from_store
from dotenv import dotenv_values
from lpcraft import env
+from lpcraft.commands.clean import clean
from lpcraft.config import Config, Job, Output
from lpcraft.errors import CommandError
from lpcraft.plugin.manager import get_plugin_manager
@@ -283,34 +284,40 @@ def run(args: Namespace) -> int:
provider = get_provider()
provider.ensure_provider_is_available()
+ try:
+ for stage in config.pipeline:
+ stage_failed = False
+ for job_name in stage:
+ try:
+ jobs = config.jobs.get(job_name, [])
+ if not jobs:
+ raise CommandError(
+ f"No job definition for {job_name!r}"
+ )
+ for job in jobs:
+ _run_job(
+ job_name,
+ job,
+ provider,
+ getattr(args, "output_directory", None),
+ )
+ except CommandError as e:
+ if len(stage) == 1:
+ # Single-job stage, so just reraise this
+ # in order to get simpler error messages.
+ raise
+ else:
+ emit.error(e)
+ stage_failed = True
+ if stage_failed:
+ raise CommandError(
+ f"Some jobs in {stage} failed; stopping.", retcode=1
+ )
+ finally:
+ clean_environment = getattr(args, "clean", False)
- for stage in config.pipeline:
- stage_failed = False
- for job_name in stage:
- try:
- jobs = config.jobs.get(job_name, [])
- if not jobs:
- raise CommandError(f"No job definition for {job_name!r}")
- for job in jobs:
- _run_job(
- job_name,
- job,
- provider,
- getattr(args, "output_directory", None),
- )
- except CommandError as e:
- if len(stage) == 1:
- # Single-job stage, so just reraise this in order to get
- # simpler error messages.
- raise
- else:
- emit.error(e)
- stage_failed = True
- if stage_failed:
- raise CommandError(
- f"Some jobs in {stage} failed; stopping.", retcode=1
- )
-
+ if clean_environment:
+ clean(args)
return 0
@@ -333,6 +340,12 @@ def run_one(args: Namespace) -> int:
provider = get_provider()
provider.ensure_provider_is_available()
- _run_job(args.job, job, provider, getattr(args, "output", None))
+ try:
+ _run_job(args.job, job, provider, getattr(args, "output", None))
+ finally:
+ clean_environment = getattr(args, "clean", False)
+
+ if clean_environment:
+ clean(args)
return 0
diff --git a/lpcraft/commands/tests/__init__.py b/lpcraft/commands/tests/__init__.py
index aba747f..f21c05a 100644
--- a/lpcraft/commands/tests/__init__.py
+++ b/lpcraft/commands/tests/__init__.py
@@ -2,13 +2,16 @@
# GNU General Public License version 3 (see the file LICENSE).
from dataclasses import dataclass
-from typing import List
-from unittest.mock import ANY, call
+from typing import List, Optional
+from unittest.mock import ANY, Mock, call
from craft_cli import CraftError
+from craft_providers.lxd import LXC, launch
from testtools import TestCase
from lpcraft.main import main
+from lpcraft.providers._lxd import LXDProvider, _LXDLauncher
+from lpcraft.providers.tests import FakeLXDInstaller
from lpcraft.tests.fixtures import RecordingEmitterFixture
@@ -45,3 +48,19 @@ class CommandBaseTestCase(TestCase):
],
)
return result
+
+
+def makeLXDProvider(
+ is_ready: bool = True,
+ lxd_launcher: Optional[_LXDLauncher] = None,
+) -> LXDProvider:
+ lxc = Mock(spec=LXC)
+ lxc.remote_list.return_value = {}
+ lxd_installer = FakeLXDInstaller(is_ready=is_ready)
+ if lxd_launcher is None:
+ lxd_launcher = Mock(spec=launch)
+ return LXDProvider(
+ lxc=lxc,
+ lxd_installer=lxd_installer,
+ lxd_launcher=lxd_launcher,
+ )
diff --git a/lpcraft/commands/tests/test_clean.py b/lpcraft/commands/tests/test_clean.py
new file mode 100644
index 0000000..cd042d8
--- /dev/null
+++ b/lpcraft/commands/tests/test_clean.py
@@ -0,0 +1,98 @@
+# Copyright 2022 Canonical Ltd. This software is licensed under the
+# GNU General Public License version 3 (see the file LICENSE).
+
+import os
+from pathlib import Path
+from textwrap import dedent
+from unittest.mock import patch
+
+from fixtures import TempDir
+from testtools.matchers import MatchesStructure
+
+from lpcraft.commands.tests import CommandBaseTestCase, makeLXDProvider
+from lpcraft.errors import CommandError, ConfigurationError
+
+
+class TestClean(CommandBaseTestCase):
+ def setUp(self):
+ super().setUp()
+ self.tmp_project_path = Path(
+ self.useFixture(TempDir()).join("test-clean-project")
+ )
+ self.tmp_project_path.mkdir()
+ cwd = Path.cwd()
+ os.chdir(self.tmp_project_path)
+ self.addCleanup(os.chdir, cwd)
+
+ def test_missing_config_file(self):
+ result = self.run_command("clean")
+
+ self.assertThat(
+ result,
+ MatchesStructure.byEquality(
+ exit_code=1,
+ errors=[
+ ConfigurationError(
+ "Couldn't find config file '.launchpad.yaml'"
+ ),
+ ],
+ ),
+ )
+ tmp_config_file = os.path.join(
+ self.tmp_project_path, "test-clean-config/lpcraft-configuration.yaml"
+ )
+ result = self.run_command("clean", "-c", tmp_config_file)
+
+ self.assertThat(
+ result,
+ MatchesStructure.byEquality(
+ exit_code=1,
+ errors=[
+ ConfigurationError(
+ f"Couldn't find config file '{tmp_config_file}'"
+ )
+ ],
+ )
+ )
+
+ @patch("lpcraft.commands.clean.get_provider")
+ def test_lxd_not_ready(
+ self, mock_get_provider
+ ):
+ mock_get_provider.return_value = makeLXDProvider(is_ready=False)
+ config = dedent(
+ """
+ pipeline: []
+ jobs: {}
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ result = self.run_command("clean")
+
+ self.assertThat(
+ result,
+ MatchesStructure.byEquality(
+ exit_code=1,
+ errors=[CommandError("LXD is broken")],
+ ),
+ )
+
+ @patch("lpcraft.commands.clean.get_provider")
+ @patch("lpcraft.providers._lxd.LXDProvider.clean_project_environments")
+ def test_clean_cleans_project_environments(
+ self, mock_clean_project_environments, mock_get_provider
+ ):
+ mock_get_provider.return_value = makeLXDProvider(is_ready=True)
+
+ config = dedent(
+ """
+ pipeline: []
+ jobs: {}
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+ self.run_command("clean")
+ mock_clean_project_environments.assert_called_with(
+ project_name=self.tmp_project_path.name, project_path=self.tmp_project_path
+ )
diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py
index 19cb494..dc35f54 100644
--- a/lpcraft/commands/tests/test_run.py
+++ b/lpcraft/commands/tests/test_run.py
@@ -11,14 +11,12 @@ from textwrap import dedent
from typing import Any, AnyStr, Dict, List, Optional
from unittest.mock import ANY, Mock, call, patch
-from craft_providers.lxd import LXC, launch
+from craft_providers.lxd import launch
from fixtures import TempDir
from testtools.matchers import MatchesStructure
-from lpcraft.commands.tests import CommandBaseTestCase
+from lpcraft.commands.tests import CommandBaseTestCase, makeLXDProvider
from lpcraft.errors import CommandError, ConfigurationError
-from lpcraft.providers._lxd import LXDProvider, _LXDLauncher
-from lpcraft.providers.tests import FakeLXDInstaller
class LocalExecuteRun:
@@ -71,22 +69,6 @@ class RunBaseTestCase(CommandBaseTestCase):
self.addCleanup(os.chdir, cwd)
- def makeLXDProvider(
- self,
- is_ready: bool = True,
- lxd_launcher: Optional[_LXDLauncher] = None,
- ) -> LXDProvider:
- lxc = Mock(spec=LXC)
- lxc.remote_list.return_value = {}
- lxd_installer = FakeLXDInstaller(is_ready=is_ready)
- if lxd_launcher is None:
- lxd_launcher = Mock(spec=launch)
- return LXDProvider(
- lxc=lxc,
- lxd_installer=lxd_installer,
- lxd_launcher=lxd_launcher,
- )
-
class TestRun(RunBaseTestCase):
def test_missing_config_file(self):
@@ -116,7 +98,7 @@ class TestRun(RunBaseTestCase):
)
Path(self.tmp_config_path).mkdir(parents=True, exist_ok=True)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -150,7 +132,7 @@ class TestRun(RunBaseTestCase):
def test_lxd_not_ready(
self, mock_get_host_architecture, mock_get_provider
):
- mock_get_provider.return_value = self.makeLXDProvider(is_ready=False)
+ mock_get_provider.return_value = makeLXDProvider(is_ready=False)
config = dedent(
"""
pipeline: []
@@ -174,7 +156,7 @@ class TestRun(RunBaseTestCase):
def test_job_not_defined(
self, mock_get_host_architecture, mock_get_provider
):
- mock_get_provider.return_value = self.makeLXDProvider()
+ mock_get_provider.return_value = makeLXDProvider()
config = dedent(
"""
pipeline:
@@ -204,7 +186,7 @@ class TestRun(RunBaseTestCase):
# assumed that the dispatcher won't dispatch anything for an
# architecture if it has no jobs at all.)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -252,7 +234,7 @@ class TestRun(RunBaseTestCase):
def test_no_run_definition(
self, mock_get_host_architecture, mock_get_provider
):
- mock_get_provider.return_value = self.makeLXDProvider()
+ mock_get_provider.return_value = makeLXDProvider()
config = dedent(
"""
pipeline:
@@ -286,7 +268,7 @@ class TestRun(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 2)
@@ -338,7 +320,7 @@ class TestRun(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -402,7 +384,7 @@ class TestRun(RunBaseTestCase):
# calling `lpcraft` with no arguments triggers the run command
# and is functionally equivalent to `lpcraft run`
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -446,7 +428,7 @@ class TestRun(RunBaseTestCase):
# one job in a stage fails, we run all the jobs in that stage before
# stopping.
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.side_effect = iter(
@@ -520,7 +502,7 @@ class TestRun(RunBaseTestCase):
# but we do at least wait for all of them to succeed before
# proceeding to the next stage in the pipeline.
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.side_effect = iter(
@@ -576,7 +558,7 @@ class TestRun(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -648,7 +630,7 @@ class TestRun(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -698,7 +680,7 @@ class TestRun(RunBaseTestCase):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -761,7 +743,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -805,7 +787,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -850,7 +832,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -898,7 +880,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -945,7 +927,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -990,7 +972,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -1035,7 +1017,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -1085,7 +1067,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -1129,7 +1111,7 @@ class TestRun(RunBaseTestCase):
):
target_path = Path(self.useFixture(TempDir()).path)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = LocalExecuteRun(self.tmp_project_path)
launcher.return_value.execute_run = execute_run
@@ -1169,7 +1151,7 @@ class TestRun(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -1256,7 +1238,7 @@ class TestRun(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -1310,7 +1292,7 @@ class TestRun(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 100)
@@ -1358,7 +1340,7 @@ class TestRun(RunBaseTestCase):
return subprocess.CompletedProcess([], 0)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
launcher.return_value.execute_run.side_effect = execute_run
config = dedent(
@@ -1392,7 +1374,7 @@ class TestRun(RunBaseTestCase):
return subprocess.CompletedProcess([], 0)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
launcher.return_value.execute_run.side_effect = execute_run
config = dedent(
@@ -1428,6 +1410,82 @@ class TestRun(RunBaseTestCase):
stderr_lines[-2:],
)
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ @patch("lpcraft.providers._lxd.LXDProvider.clean_project_environments")
+ def test_clean_flag_always_cleans_up_even_when_there_are_errors(
+ self,
+ mock_clean_project_environments,
+ mock_get_host_architecture,
+ mock_get_provider,
+ ):
+ def execute_run(
+ command: List[str], **kwargs: Any
+ ) -> "subprocess.CompletedProcess[AnyStr]":
+ os.write(kwargs["stdout"], b"test\n")
+ return subprocess.CompletedProcess([], 0)
+
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ launcher.return_value.execute_run.side_effect = execute_run
+ config = dedent(
+ """
+ pipeline:
+ - test
+
+ jobs:
+ test:
+ series: focal
+ architectures: amd64
+ run: echo test
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ result = self.run_command("run", "--clean")
+ self.assertEqual(0, result.exit_code)
+
+ mock_clean_project_environments.assert_called_with(
+ project_name=self.tmp_project_path.name,
+ project_path=self.tmp_project_path,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ @patch("lpcraft.providers._lxd.LXDProvider.clean_project_environments")
+ def test_clean_flag_cleans_up_the_managed_environment(
+ self,
+ mock_clean_project_environments,
+ mock_get_host_architecture,
+ mock_get_provider,
+ ):
+ mock_get_provider.return_value = makeLXDProvider()
+ # There are no jobs defined. So there will be an error
+ config = dedent(
+ """
+ pipeline:
+ - test
+
+ jobs: {}
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+ result = self.run_command("run", "--clean")
+
+ self.assertThat(
+ result,
+ MatchesStructure.byEquality(
+ exit_code=1,
+ errors=[CommandError("No job definition for 'test'")],
+ ),
+ )
+
+ mock_clean_project_environments.assert_called_with(
+ project_name=self.tmp_project_path.name,
+ project_path=self.tmp_project_path,
+ )
+
class TestRunOne(RunBaseTestCase):
def test_missing_config_file(self):
@@ -1457,7 +1515,7 @@ class TestRunOne(RunBaseTestCase):
)
Path(self.tmp_config_path).mkdir(parents=True, exist_ok=True)
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -1499,7 +1557,7 @@ class TestRunOne(RunBaseTestCase):
def test_job_not_defined(
self, mock_get_host_architecture, mock_get_provider
):
- mock_get_provider.return_value = self.makeLXDProvider()
+ mock_get_provider.return_value = makeLXDProvider()
config = dedent(
"""
pipeline:
@@ -1525,7 +1583,7 @@ class TestRunOne(RunBaseTestCase):
def test_job_index_not_defined(
self, mock_get_host_architecture, mock_get_provider
):
- mock_get_provider.return_value = self.makeLXDProvider()
+ mock_get_provider.return_value = makeLXDProvider()
config = dedent(
"""
pipeline:
@@ -1556,7 +1614,7 @@ class TestRunOne(RunBaseTestCase):
@patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
def test_job_fails(self, mock_get_host_architecture, mock_get_provider):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 2)
@@ -1608,7 +1666,7 @@ class TestRunOne(RunBaseTestCase):
self, mock_get_host_architecture, mock_get_provider
):
launcher = Mock(spec=launch)
- provider = self.makeLXDProvider(lxd_launcher=launcher)
+ provider = makeLXDProvider(lxd_launcher=launcher)
mock_get_provider.return_value = provider
execute_run = launcher.return_value.execute_run
execute_run.return_value = subprocess.CompletedProcess([], 0)
@@ -1649,3 +1707,98 @@ class TestRunOne(RunBaseTestCase):
stdout=ANY,
stderr=ANY,
)
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ @patch("lpcraft.providers._lxd.LXDProvider.clean_project_environments")
+ def test_run_one_clean_flag_always_cleans_up_even_when_there_are_errors(
+ self,
+ mock_clean_project_environments,
+ mock_get_host_architecture,
+ mock_get_provider,
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 2)
+ config = dedent(
+ """
+ pipeline:
+ - test
+ - build-wheel
+
+ jobs:
+ test:
+ series: focal
+ architectures: amd64
+ run: tox
+ build-wheel:
+ series: focal
+ architectures: amd64
+ run: pyproject-build
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+ result = self.run_command("run-one", "--clean", "build-wheel", "0")
+ self.assertThat(
+ result,
+ MatchesStructure.byEquality(
+ exit_code=2,
+ errors=[
+ CommandError(
+ "Job 'build-wheel' for focal/amd64 failed with exit "
+ "status 2.",
+ retcode=2,
+ )
+ ],
+ ),
+ )
+ mock_clean_project_environments.assert_called_with(
+ project_name=self.tmp_project_path.name,
+ project_path=self.tmp_project_path,
+ )
+
+ @patch("lpcraft.commands.run.get_provider")
+ @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+ @patch("lpcraft.providers._lxd.LXDProvider.clean_project_environments")
+ def test_run_one_clean_flag_cleans_up_the_managed_environment(
+ self,
+ mock_clean_project_environments,
+ mock_get_host_architecture,
+ mock_get_provider,
+ ):
+ launcher = Mock(spec=launch)
+ provider = makeLXDProvider(lxd_launcher=launcher)
+ mock_get_provider.return_value = provider
+ execute_run = launcher.return_value.execute_run
+ execute_run.return_value = subprocess.CompletedProcess([], 0)
+ config = dedent(
+ """
+ pipeline:
+ - test
+ - build-wheel
+
+ jobs:
+ test:
+ matrix:
+ - series: bionic
+ architectures: amd64
+ - series: focal
+ architectures: [amd64, s390x]
+ run: tox
+ build-wheel:
+ series: bionic
+ architectures: amd64
+ run: pyproject-build
+ """
+ )
+ Path(".launchpad.yaml").write_text(config)
+
+ result = self.run_command("run-one", "--clean", "test", "1")
+
+ self.assertEqual(0, result.exit_code)
+ mock_clean_project_environments.assert_called_with(
+ project_name=self.tmp_project_path.name,
+ project_path=self.tmp_project_path,
+ )
diff --git a/lpcraft/main.py b/lpcraft/main.py
index 8afe421..028dfc5 100644
--- a/lpcraft/main.py
+++ b/lpcraft/main.py
@@ -11,6 +11,7 @@ from typing import List, Optional
from craft_cli import CraftError, EmitterMode, emit
from lpcraft._version import version_description as lpcraft_version
+from lpcraft.commands.clean import clean
from lpcraft.commands.run import run, run_one
from lpcraft.commands.version import version
from lpcraft.errors import CommandError
@@ -62,6 +63,16 @@ def main(argv: Optional[List[str]] = None) -> int:
# XXX cjwatson 2021-11-15: Subcommand arguments should be defined
# alongside the individual subcommands rather than here.
+ parser_clean = subparsers.add_parser("clean", help=clean.__doc__)
+ parser_clean.add_argument(
+ "-c",
+ "--config",
+ type=Path,
+ default=".launchpad.yaml",
+ help="Read the configuration file from this path.",
+ )
+ parser_clean.set_defaults(func=clean)
+
parser_run = subparsers.add_parser("run", help=run.__doc__)
parser_run.add_argument(
"--output-directory",
@@ -75,6 +86,12 @@ def main(argv: Optional[List[str]] = None) -> int:
default=".launchpad.yaml",
help="Read the configuration file from this path.",
)
+ parser_run.add_argument(
+ "--clean",
+ default=False,
+ action="store_true",
+ help="Clean the managed environments after the run.",
+ )
parser_run.set_defaults(func=run)
parser_run_one = subparsers.add_parser("run-one", help=run_one.__doc__)
@@ -90,6 +107,12 @@ def main(argv: Optional[List[str]] = None) -> int:
default=".launchpad.yaml",
help="Read the configuration file from this path.",
)
+ parser_run_one.add_argument(
+ "--clean",
+ default=False,
+ action="store_true",
+ help="Clean the managed environments after the run.",
+ )
parser_run_one.add_argument("job", help="Run only this job name.")
parser_run_one.add_argument(
"index",
Follow ups