← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/lpcraft:add-additional-apt-repositories into lpcraft:main

 

Jürgen Gmach has proposed merging ~jugmac00/lpcraft:add-additional-apt-repositories into lpcraft:main.

Commit message:
Add new configuration option to provide additional apt repositories

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/lpcraft/+git/lpcraft/+merge/425829
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/lpcraft:add-additional-apt-repositories into lpcraft:main.
diff --git a/NEWS.rst b/NEWS.rst
index 72603d6..8f0d35c 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -9,6 +9,8 @@ Version history
 
 - Hide the internal ``run-one`` command from ``--help`` output.
 
+- Add new configuration option to provide additional apt repositories.
+
 0.0.17 (2022-06-17)
 ===================
 
diff --git a/docs/configuration.rst b/docs/configuration.rst
index ecaa727..c4c5af4 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -50,6 +50,10 @@ Job definitions
 ``packages`` (optional)
     Packages to install using ``apt`` as dependencies of this job.
 
+``additional-apt-repositories`` (optional)
+    Repositories which will be added to the already existing ones in
+    `/etc/apt/sources.list`.
+
 ``snaps`` (optional)
     Snaps to install as dependencies of this job.
 
diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py
index dd39c1c..60925af 100644
--- a/lpcraft/commands/run.py
+++ b/lpcraft/commands/run.py
@@ -9,6 +9,7 @@ import os
 import shlex
 from argparse import ArgumentParser, Namespace
 from pathlib import Path, PurePath
+from tempfile import mkstemp
 from typing import Dict, List, Optional, Set
 
 from craft_cli import BaseCommand, EmitterMode, emit
@@ -226,19 +227,33 @@ def _install_apt_packages(
     host_architecture: str,
     remote_cwd: Path,
     apt_replacement_repositories: Optional[List[str]],
+    additional_apt_repositories: Optional[List[str]],
     environment: Optional[Dict[str, Optional[str]]],
 ) -> None:
-    if apt_replacement_repositories:
-        # replace sources.list
-        lines = "\n".join(apt_replacement_repositories) + "\n"
+    if apt_replacement_repositories or additional_apt_repositories:
+        sources_list_path = "/etc/apt/sources.list"
+        _, tmpfile = mkstemp()
+        try:
+            instance.pull_file(
+                source=Path(sources_list_path), destination=Path(tmpfile)
+            )
+        except Exception as e:
+            raise CommandError(str(e), retcode=1)
+        with open(tmpfile) as f:
+            sources = f.read()
+        if apt_replacement_repositories:
+            sources = "\n".join(apt_replacement_repositories) + "\n"
+        if additional_apt_repositories:
+            sources += "\n" + "\n".join(additional_apt_repositories) + "\n"
         with emit.open_stream("Replacing /etc/apt/sources.list") as stream:
             instance.push_file_io(
-                destination=PurePath("/etc/apt/sources.list"),
-                content=io.BytesIO(lines.encode()),
+                destination=PurePath(sources_list_path),
+                content=io.BytesIO(sources.encode()),
                 file_mode="0644",
                 group="root",
                 user="root",
             )
+
     # update local repository information
     apt_update = ["apt", "update"]
     with emit.open_stream(f"Running {apt_update}") as stream:
@@ -316,6 +331,7 @@ def _run_job(
     provider: Provider,
     output: Optional[Path],
     apt_replacement_repositories: Optional[List[str]] = None,
+    additional_apt_repositories: Optional[List[str]] = None,
     env_from_cli: Optional[List[str]] = None,
     plugin_settings: Optional[List[str]] = None,
 ) -> None:
@@ -406,6 +422,7 @@ def _run_job(
                 host_architecture=host_architecture,
                 remote_cwd=remote_cwd,
                 apt_replacement_repositories=apt_replacement_repositories,
+                additional_apt_repositories=additional_apt_repositories,
                 environment=environment,
             )
         for cmd in (pre_run_command, run_command, post_run_command):
@@ -518,6 +535,7 @@ class RunCommand(BaseCommand):
                                 apt_replacement_repositories=(
                                     args.apt_replace_repositories
                                 ),
+                                additional_apt_repositories=job.additional_apt_repositories,  # noqa: E501
                                 env_from_cli=args.set_env,
                                 plugin_settings=args.plugin_setting,
                             )
@@ -625,6 +643,7 @@ class RunOneCommand(BaseCommand):
                 provider,
                 args.output_directory,
                 apt_replacement_repositories=args.apt_replace_repositories,
+                additional_apt_repositories=job.additional_apt_repositories,
                 env_from_cli=args.set_env,
                 plugin_settings=args.plugin_setting,
             )
diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py
index 165d435..86c6df5 100644
--- a/lpcraft/commands/tests/test_run.py
+++ b/lpcraft/commands/tests/test_run.py
@@ -1995,6 +1995,89 @@ class TestRun(RunBaseTestCase):
             execute_run.call_args_list,
         )
 
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_run_with_additional_apt_repositories(
+        self, mock_get_host_architecture, mock_get_provider
+    ):
+        existing_repositories = [
+            "deb http://archive.ubuntu.com/ubuntu/ focal main restricted",
+            "deb-src http://archive.ubuntu.com/ubuntu/ focal main restricted",
+        ]
+
+        def fake_pull_file(source: Path, destination: Path) -> None:
+            destination.write_text("\n".join(existing_repositories))
+
+        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)
+        launcher.return_value.pull_file.side_effect = fake_pull_file
+        additional_repositories = [
+            "deb https://canonical.example.org/artifactory/jammy-golang-backport focal main",  # noqa: E501
+            "deb https://canonical.example.org/artifactory/jammy-golang-backport focal universe",  # noqa: E501
+        ]
+        config = dedent(
+            f"""
+            pipeline:
+                - test
+            jobs:
+                test:
+                    series: focal
+                    architectures: amd64
+                    run: ls -la
+                    packages: [git]
+                    additional-apt-repositories: {additional_repositories}
+            """
+        )
+        Path(".launchpad.yaml").write_text(config)
+
+        result = self.run_command("run")
+
+        self.assertEqual(0, result.exit_code)
+        self.assertEqual(
+            [
+                call(
+                    ["apt", "update"],
+                    cwd=Path("/root/lpcraft/project"),
+                    env={},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    ["apt", "install", "-y", "git"],
+                    cwd=Path("/root/lpcraft/project"),
+                    env={},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    ["bash", "--noprofile", "--norc", "-ec", "ls -la"],
+                    cwd=Path("/root/lpcraft/project"),
+                    env={},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+            ],
+            execute_run.call_args_list,
+        )
+        mock_info = launcher.return_value.push_file_io.call_args_list
+
+        self.assertEqual(
+            Path("/etc/apt/sources.list"), mock_info[0][1]["destination"]
+        )
+
+        file_contents = mock_info[0][1]["content"].read().decode()
+
+        self.assertEqual(
+            "\n".join(existing_repositories)
+            + "\n"
+            + "\n".join(additional_repositories)
+            + "\n",
+            file_contents,
+        )
+
 
 class TestRunOne(RunBaseTestCase):
     def test_config_file_not_under_project_directory(self):
@@ -2637,3 +2720,131 @@ class TestRunOne(RunBaseTestCase):
             ],
             execute_run.call_args_list,
         )
+
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_run_with_additional_apt_repositories(
+        self, mock_get_host_architecture, mock_get_provider
+    ):
+        existing_repositories = [
+            "deb http://archive.ubuntu.com/ubuntu/ focal main restricted",
+            "deb-src http://archive.ubuntu.com/ubuntu/ focal main restricted",
+        ]
+
+        def fake_pull_file(source: Path, destination: Path) -> None:
+            destination.write_text("\n".join(existing_repositories))
+
+        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)
+        launcher.return_value.pull_file.side_effect = fake_pull_file
+        additional_repositories = [
+            "deb https://canonical.example.org/artifactory/jammy-golang-backport focal main",  # noqa: E501
+            "deb https://canonical.example.org/artifactory/jammy-golang-backport focal universe",  # noqa: E501
+        ]
+        config = dedent(
+            f"""
+            pipeline:
+                - test
+            jobs:
+                test:
+                    series: focal
+                    architectures: amd64
+                    run: ls -la
+                    packages: [git]
+                    additional-apt-repositories: {additional_repositories}
+            """
+        )
+        Path(".launchpad.yaml").write_text(config)
+
+        result = self.run_command("run-one", "test", "0")
+
+        self.assertEqual(0, result.exit_code)
+        self.assertEqual(
+            [
+                call(
+                    ["apt", "update"],
+                    cwd=Path("/root/lpcraft/project"),
+                    env={},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    ["apt", "install", "-y", "git"],
+                    cwd=Path("/root/lpcraft/project"),
+                    env={},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+                call(
+                    ["bash", "--noprofile", "--norc", "-ec", "ls -la"],
+                    cwd=Path("/root/lpcraft/project"),
+                    env={},
+                    stdout=ANY,
+                    stderr=ANY,
+                ),
+            ],
+            execute_run.call_args_list,
+        )
+        mock_info = launcher.return_value.push_file_io.call_args_list
+
+        self.assertEqual(
+            Path("/etc/apt/sources.list"), mock_info[0][1]["destination"]
+        )
+
+        file_contents = mock_info[0][1]["content"].read().decode()
+
+        self.assertEqual(
+            "\n".join(existing_repositories)
+            + "\n"
+            + "\n".join(additional_repositories)
+            + "\n",
+            file_contents,
+        )
+
+    @patch("lpcraft.env.get_managed_environment_project_path")
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_fails_pulling_sources_list(
+        self,
+        mock_get_host_architecture,
+        mock_get_provider,
+        mock_get_project_path,
+    ):
+        launcher = Mock(spec=launch)
+        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
+        launcher.return_value.pull_file.side_effect = FileNotFoundError(
+            "File not found"
+        )
+        additional_repositories = [
+            "deb https://canonical.example.org/artifactory/jammy-golang-backport focal main",  # noqa: E501
+            "deb https://canonical.example.org/artifactory/jammy-golang-backport focal universe",  # noqa: E501
+        ]
+        config = dedent(
+            f"""
+            pipeline:
+                - test
+            jobs:
+                test:
+                    series: focal
+                    architectures: amd64
+                    run: ls -la
+                    packages: [git]
+                    additional-apt-repositories: {additional_repositories}
+            """
+        )
+        Path(".launchpad.yaml").write_text(config)
+
+        result = self.run_command("run-one", "test", "0")
+
+        self.assertThat(
+            result,
+            MatchesStructure.byEquality(
+                exit_code=1, errors=[CommandError("File not found", retcode=1)]
+            ),
+        )
diff --git a/lpcraft/config.py b/lpcraft/config.py
index 0a4120d..f4e37cc 100644
--- a/lpcraft/config.py
+++ b/lpcraft/config.py
@@ -92,6 +92,7 @@ class Job(ModelConfigDefaults):
     output: Optional[Output]
     snaps: Optional[List[StrictStr]]
     packages: Optional[List[StrictStr]]
+    additional_apt_repositories: Optional[List[StrictStr]]
     plugin: Optional[StrictStr]
     plugin_config: Optional[BaseConfig]
 
diff --git a/lpcraft/tests/test_config.py b/lpcraft/tests/test_config.py
index d34b718..6e039c0 100644
--- a/lpcraft/tests/test_config.py
+++ b/lpcraft/tests/test_config.py
@@ -403,3 +403,29 @@ class TestConfig(TestCase):
         config = Config.load(path)
 
         self.assertEqual("tox", config.jobs["test"][0].plugin)
+
+    def test_additional_apt_repositories(self):
+        path = self.create_config(
+            dedent(
+                """
+                pipeline:
+                    - test
+
+                jobs:
+                    test:
+                        series: focal
+                        architectures: amd64
+                        packages: [nginx, apache2]
+                        additional-apt-repositories: ["deb https://canonical.example.org/artifactory/jammy-golang-backport focal main"]
+                """  # noqa: E501
+            )
+        )
+
+        config = Config.load(path)
+
+        self.assertEqual(
+            [
+                "deb https://canonical.example.org/artifactory/jammy-golang-backport focal main"  # noqa: E501
+            ],
+            config.jobs["test"][0].additional_apt_repositories,
+        )

Follow ups