← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/lpcraft:pass-in-credentials into lpcraft:main

 

Jürgen Gmach has proposed merging ~jugmac00/lpcraft:pass-in-credentials into lpcraft:main with ~jugmac00/lpcraft:add-additional-apt-repositories as a prerequisite.

Commit message:
Add new CLI option to provide credentials

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/lpcraft/+git/lpcraft/+merge/425865
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/lpcraft:pass-in-credentials into lpcraft:main.
diff --git a/NEWS.rst b/NEWS.rst
index 8f0d35c..e41f7b5 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -11,6 +11,8 @@ Version history
 
 - Add new configuration option to provide additional apt repositories.
 
+- Add new CLI option to provide credentials.
+
 0.0.17 (2022-06-17)
 ===================
 
diff --git a/docs/cli-interface.rst b/docs/cli-interface.rst
index 45a3914..3d17527 100644
--- a/docs/cli-interface.rst
+++ b/docs/cli-interface.rst
@@ -29,6 +29,9 @@ lpcraft run optional arguments
 - ``--plugin-setting``, e.g.
   ``lpcraft run --plugin-setting="foo=bar"``
 
+- ``--credentials``, e.g.
+  ``lpcraft run --credentials="user:pass"``
+
 lpcraft run-one
 ---------------
 
@@ -52,3 +55,6 @@ lpcraft run-one optional arguments
 
 - ``--plugin-setting``, e.g.
   ``lpcraft run-one --plugin-setting="foo=bar" test 0``.
+
+- ``--credentials``, e.g.
+  ``lpcraft run-one --credentials="user:pass" test 0``.
diff --git a/docs/configuration.rst b/docs/configuration.rst
index c4c5af4..a51c835 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -52,7 +52,8 @@ Job definitions
 
 ``additional-apt-repositories`` (optional)
     Repositories which will be added to the already existing ones in
-    `/etc/apt/sources.list`.
+    `/etc/apt/sources.list`. May contain a placeholder (`<credentials>`) for
+    authentication credentials.
 
 ``snaps`` (optional)
     Snaps to install as dependencies of this job.
diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py
index 60925af..96c5a9e 100644
--- a/lpcraft/commands/run.py
+++ b/lpcraft/commands/run.py
@@ -229,6 +229,7 @@ def _install_apt_packages(
     apt_replacement_repositories: Optional[List[str]],
     additional_apt_repositories: Optional[List[str]],
     environment: Optional[Dict[str, Optional[str]]],
+    credentials: Optional[str],
 ) -> None:
     if apt_replacement_repositories or additional_apt_repositories:
         sources_list_path = "/etc/apt/sources.list"
@@ -244,6 +245,11 @@ def _install_apt_packages(
         if apt_replacement_repositories:
             sources = "\n".join(apt_replacement_repositories) + "\n"
         if additional_apt_repositories:
+            if credentials:
+                additional_apt_repositories = [
+                    repository.replace("<credentials>", credentials)
+                    for repository in 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(
@@ -334,6 +340,7 @@ def _run_job(
     additional_apt_repositories: Optional[List[str]] = None,
     env_from_cli: Optional[List[str]] = None,
     plugin_settings: Optional[List[str]] = None,
+    credentials: Optional[str] = None,
 ) -> None:
     """Run a single job."""
     # XXX jugmac00 2022-04-27: we should create a configuration object to be
@@ -424,6 +431,7 @@ def _run_job(
                 apt_replacement_repositories=apt_replacement_repositories,
                 additional_apt_repositories=additional_apt_repositories,
                 environment=environment,
+                credentials=credentials,
             )
         for cmd in (pre_run_command, run_command, post_run_command):
             if cmd:
@@ -504,6 +512,10 @@ class RunCommand(BaseCommand):
             action="append",
             help="Set an environment variable.",
         )
+        parser.add_argument(
+            "--credentials",
+            help="Pass in credentials.",
+        )
 
     def run(self, args: Namespace) -> int:
         """Run the command."""
@@ -527,6 +539,7 @@ class RunCommand(BaseCommand):
                             launched_instances.append(
                                 _get_job_instance_name(provider, job)
                             )
+                            # breakpoint()
                             _run_job(
                                 job_name,
                                 job,
@@ -538,6 +551,7 @@ class RunCommand(BaseCommand):
                                 additional_apt_repositories=job.additional_apt_repositories,  # noqa: E501
                                 env_from_cli=args.set_env,
                                 plugin_settings=args.plugin_setting,
+                                credentials=args.credentials,
                             )
                     except CommandError as e:
                         if len(stage) == 1:
@@ -619,6 +633,10 @@ class RunOneCommand(BaseCommand):
             action="append",
             help="Set an environment variable.",
         )
+        parser.add_argument(
+            "--credentials",
+            help="Pass in credentials.",
+        )
 
     def run(self, args: Namespace) -> int:
         """Run the command."""
@@ -646,6 +664,7 @@ class RunOneCommand(BaseCommand):
                 additional_apt_repositories=job.additional_apt_repositories,
                 env_from_cli=args.set_env,
                 plugin_settings=args.plugin_setting,
+                credentials=args.credentials,
             )
         finally:
             if args.clean:
diff --git a/lpcraft/commands/tests/test_run.py b/lpcraft/commands/tests/test_run.py
index 86c6df5..3446d8d 100644
--- a/lpcraft/commands/tests/test_run.py
+++ b/lpcraft/commands/tests/test_run.py
@@ -2078,6 +2078,98 @@ class TestRun(RunBaseTestCase):
             file_contents,
         )
 
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_run_with_additional_apt_repositories_with_credentials(
+        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://<credentials>@canonical.example.org/artifactory/jammy-golang-backport focal main",  # noqa: E501
+            "deb https://<credentials>@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",
+            "--credentials",
+            "user:pass",
+        )
+
+        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()
+
+        updated_additional_repositories = [
+            "deb https://user:pass@xxxxxxxxxxxxxxxxxxxxx/artifactory/jammy-golang-backport focal main",  # noqa: E501
+            "deb https://user:pass@xxxxxxxxxxxxxxxxxxxxx/artifactory/jammy-golang-backport focal universe",  # noqa: E501
+        ]
+
+        self.assertEqual(
+            "\n".join(existing_repositories)
+            + "\n"
+            + "\n".join(updated_additional_repositories)
+            + "\n",
+            file_contents,
+        )
+
 
 class TestRunOne(RunBaseTestCase):
     def test_config_file_not_under_project_directory(self):
@@ -2848,3 +2940,130 @@ class TestRunOne(RunBaseTestCase):
                 exit_code=1, errors=[CommandError("File not found", retcode=1)]
             ),
         )
+
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_provide_credentials_via_cli(
+        self, 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
+            jobs:
+                test:
+                    series: focal
+                    architectures: amd64
+                    run: tox
+            """
+        )
+        Path(".launchpad.yaml").write_text(config)
+        result = self.run_command(
+            "run-one",
+            "--credentials",
+            "user:pass",
+            "test",
+            "0",
+        )
+        self.assertEqual(0, result.exit_code)
+
+        self.assertIn("'--credentials', 'user:pass'", result.trace[0])
+
+    @patch("lpcraft.commands.run.get_provider")
+    @patch("lpcraft.commands.run.get_host_architecture", return_value="amd64")
+    def test_run_with_additional_apt_repositories_with_credentials(
+        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://<credentials>@canonical.example.org/artifactory/jammy-golang-backport focal main",  # noqa: E501
+            "deb https://<credentials>@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",
+            "--credentials",
+            "user:pass",
+            "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()
+
+        updated_additional_repositories = [
+            "deb https://user:pass@xxxxxxxxxxxxxxxxxxxxx/artifactory/jammy-golang-backport focal main",  # noqa: E501
+            "deb https://user:pass@xxxxxxxxxxxxxxxxxxxxx/artifactory/jammy-golang-backport focal universe",  # noqa: E501
+        ]
+
+        self.assertEqual(
+            "\n".join(existing_repositories)
+            + "\n"
+            + "\n".join(updated_additional_repositories)
+            + "\n",
+            file_contents,
+        )
diff --git a/tox.ini b/tox.ini
index 71f524e..1d9be16 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,6 +6,8 @@ envlist =
     py39
     py310
     coverage
+    docs
+
 skip_missing_interpreters =
     true