diff --git a/snapcraft/services/remotebuild.py b/snapcraft/services/remotebuild.py index 3b53a25db2..c07c0dc38d 100644 --- a/snapcraft/services/remotebuild.py +++ b/snapcraft/services/remotebuild.py @@ -15,12 +15,47 @@ # along with this program. If not, see . """Snapcraft Lifecycle Service.""" +import pathlib +import craft_cli +import platformdirs from craft_application import launchpad from craft_application.services import remotebuild +from typing_extensions import override class RemoteBuild(remotebuild.RemoteBuildService): """Snapcraft remote build service.""" RecipeClass = launchpad.models.SnapRecipe + + __credentials_filepath: pathlib.Path | None = None + + @property + @override + def credentials_filepath(self) -> pathlib.Path: + """The filepath to the Launchpad credentials. + + The legacy credentials are only loaded when they exist and the new credentials + do not exist. If the legacy credentials are loaded, emit a deprecation warning. + """ + # return early so the deprecation notice is emitted only once + if self.__credentials_filepath: + return self.__credentials_filepath + + credentials_filepath = super().credentials_filepath + legacy_credentials_filepath = ( + platformdirs.user_data_path("snapcraft") / "provider/launchpad/credentials" + ) + + if not credentials_filepath.exists() and legacy_credentials_filepath.exists(): + craft_cli.emit.progress( + f"Warning: Using launchpad credentials from deprecated location {str(legacy_credentials_filepath)!r}.\n" + f"Credentials should be migrated to {str(credentials_filepath)!r}.", + permanent=True, + ) + self.__credentials_filepath = legacy_credentials_filepath + else: + self.__credentials_filepath = credentials_filepath + + return self.__credentials_filepath diff --git a/snapcraft_legacy/internal/remote_build/_launchpad.py b/snapcraft_legacy/internal/remote_build/_launchpad.py index c042fb7c48..853c35ed5e 100644 --- a/snapcraft_legacy/internal/remote_build/_launchpad.py +++ b/snapcraft_legacy/internal/remote_build/_launchpad.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2019 Canonical Ltd +# Copyright (C) 2019,2024 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 @@ -16,6 +16,8 @@ import gzip import logging +import pathlib +import platformdirs import os import shutil import time @@ -111,7 +113,6 @@ def __init__( self._cache_dir = self._create_cache_directory() self._data_dir = self._create_data_directory() - self._credentials = os.path.join(self._data_dir, "credentials") self._lp: Launchpad = self.login() self.user = self._lp.me.name @@ -122,6 +123,25 @@ def __init__( def architectures(self) -> Sequence[str]: return self._architectures + @property + def _credentials_filepath(self) -> pathlib.Path: + """The filepath to the Launchpad credentials. + + If the credentials file does not exist in the default location but exists in the + legacy location, emit a deprecation warning and return the legacy location. + """ + credentials_filepath = platformdirs.user_data_path("snapcraft") / "launchpad-credentials" + legacy_credentials_filepath = platformdirs.user_data_path("snapcraft") / "provider/launchpad/credentials" + + if not credentials_filepath.exists() and legacy_credentials_filepath.exists(): + logger.warning( + f"Warning: Using launchpad credentials from deprecated location {str(legacy_credentials_filepath)!r}.\n" + f"Credentials should be migrated to {str(credentials_filepath)!r}." + ) + return legacy_credentials_filepath + + return credentials_filepath + @architectures.setter def architectures(self, architectures: Sequence[str]) -> None: self._lp_processors: Optional[Sequence[str]] = None @@ -256,7 +276,7 @@ def login(self) -> Launchpad: "snapcraft remote-build {}".format(snapcraft_legacy.__version__), "production", self._cache_dir, - credentials_file=self._credentials, + credentials_file=self._credentials_filepath, version="devel", ) except (ConnectionRefusedError, TimeoutError): diff --git a/tests/legacy/unit/remote_build/test_launchpad.py b/tests/legacy/unit/remote_build/test_launchpad.py index 7c24733af1..ca141c47e6 100644 --- a/tests/legacy/unit/remote_build/test_launchpad.py +++ b/tests/legacy/unit/remote_build/test_launchpad.py @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2019 Canonical Ltd +# Copyright (C) 2019,2024 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 @@ -16,6 +16,8 @@ import textwrap from datetime import datetime, timedelta, timezone +import pathlib +import pytest from unittest import mock import fixtures @@ -535,3 +537,27 @@ def test_push_source_tree_error(self): self.assertRaises( errors.LaunchpadGitPushError, self.lpc.push_source_tree, repo_dir ) + + +@pytest.mark.parametrize( + ("new_exists", "legacy_exists", "expected"), + [ + (False, False, pathlib.Path("launchpad-credentials")), + (False, True, pathlib.Path("provider/launchpad/credentials")), + (True, False, pathlib.Path("launchpad-credentials")), + (True, True, pathlib.Path("launchpad-credentials")), + ], +) +def test_credentials_filepaths(new_exists, legacy_exists, expected, mocker, new_dir): + """Load legacy credentials only when they exist and the new ones do not.""" + mocker.patch.object(LaunchpadClient, "__init__", lambda x: None) + mocker.patch("platformdirs.user_data_path", return_value=new_dir) + if new_exists: + (new_dir / "launchpad-credentials").touch() + if legacy_exists: + (new_dir / "provider/launchpad/credentials").mkdir(parents=True) + (new_dir / "provider/launchpad/credentials").touch() + + credentials_filepath = LaunchpadClient()._credentials_filepath + + assert credentials_filepath == new_dir / expected diff --git a/tests/unit/services/test_remotebuild.py b/tests/unit/services/test_remotebuild.py new file mode 100644 index 0000000000..7e8db89fa3 --- /dev/null +++ b/tests/unit/services/test_remotebuild.py @@ -0,0 +1,46 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright 2024 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Snapcraft RemoteBuild service tests.""" + +import pathlib + +import pytest + + +@pytest.mark.parametrize( + ("new_exists", "legacy_exists", "expected"), + [ + (False, False, pathlib.Path("launchpad-credentials")), + (False, True, pathlib.Path("provider/launchpad/credentials")), + (True, False, pathlib.Path("launchpad-credentials")), + (True, True, pathlib.Path("launchpad-credentials")), + ], +) +def test_credentials_filepaths( + new_exists, legacy_exists, expected, remote_build_service, mocker, new_dir +): + """Load legacy credentials only when they exist and the new ones do not.""" + mocker.patch("platformdirs.user_data_path", return_value=new_dir) + if new_exists: + (new_dir / "launchpad-credentials").touch() + if legacy_exists: + (new_dir / "provider/launchpad/credentials").mkdir(parents=True) + (new_dir / "provider/launchpad/credentials").touch() + + credentials_filepath = remote_build_service.credentials_filepath + + assert credentials_filepath == new_dir / expected