Skip to content

Commit

Permalink
Merge branch 'main' into fix-register-docstring
Browse files Browse the repository at this point in the history
  • Loading branch information
facundobatista authored Nov 16, 2022
2 parents 8f1d104 + 3410375 commit bfdacd2
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 2 deletions.
17 changes: 16 additions & 1 deletion snapcraft_legacy/internal/repo/apt_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def _populate_stage_cache_dir(self) -> None:
return

# Copy apt configuration from host.
etc_apt_path = Path("/etc/apt")
cache_etc_apt_path = Path(self.stage_cache, "etc", "apt")

# Delete potentially outdated cache configuration.
Expand All @@ -120,7 +121,21 @@ def _populate_stage_cache_dir(self) -> None:

# Copy current cache configuration.
cache_etc_apt_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree("/etc/apt", cache_etc_apt_path)

# systems with ubuntu pro have an auth file inside the /etc/apt directory.
# this auth file is readable only by root, so the copytree call below may
# fail when attempting to copy this file into the cache directory
try:
shutil.copytree(etc_apt_path, cache_etc_apt_path)
except shutil.Error as error:
# copytree is a multi-file operation, so it generates a list of exceptions
# each exception in the list is a 3-element tuple: (source, dest, reason)
raise errors.PopulateCacheDirError(error.args[0]) from error
except PermissionError as error:
# catch the PermissionError raised when `/etc/apt` is unreadable
raise errors.PopulateCacheDirError(
[(etc_apt_path, cache_etc_apt_path, error)]
) from error

# Specify default arch (if specified).
if self.stage_cache_arch is not None:
Expand Down
23 changes: 22 additions & 1 deletion snapcraft_legacy/internal/repo/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from pathlib import Path
from typing import List, Optional, Sequence
from typing import List, Optional, Sequence, Tuple

from snapcraft_legacy import formatting_utils
from snapcraft_legacy.internal import errors
Expand Down Expand Up @@ -57,6 +57,27 @@ def __init__(self, errors: str) -> None:
super().__init__(errors=errors)


class PopulateCacheDirError(SnapcraftException):
def __init__(self, copy_errors: List[Tuple]) -> None:
""":param copy_errors: A list of tuples containing the copy errors, where each
tuple is ordered as (source, destination, reason)."""
self.copy_errors = copy_errors

def get_brief(self) -> str:
return "Could not populate apt cache directory."

def get_details(self) -> str:
"""Build a readable list of errors."""
details = ""
for error in self.copy_errors:
source, dest, reason = error
details += f"Unable to copy {source} to {dest}: {reason}\n"
return details

def get_resolution(self) -> str:
return "Verify user has read access to contents of /etc/apt."


class FileProviderNotFound(RepoError):

fmt = "{file_path} is not provided by any package."
Expand Down
62 changes: 62 additions & 0 deletions tests/legacy/unit/repo/test_apt_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import shutil
import unittest
from pathlib import Path
from unittest.mock import call

import fixtures
import pytest
from testtools.matchers import Equals

from snapcraft_legacy.internal.repo.apt_cache import AptCache
from snapcraft_legacy.internal.repo.errors import PopulateCacheDirError
from tests.legacy import unit


Expand Down Expand Up @@ -191,3 +194,62 @@ def test_host_get_installed_version(self):
self.assertThat(
apt_cache.get_installed_version("fake-news-bears"), Equals(None)
)


def test_populate_stage_cache_dir_shutil_error(mocker, tmp_path):
"""Raise an error when the apt cache directory cannot be populated."""
mock_copytree = mocker.patch(
"snapcraft_legacy.internal.repo.apt_cache.shutil.copytree",
side_effect=shutil.Error(
[
(
"/etc/apt/source-file-1",
"/root/.cache/dest-file-1",
"[Errno 13] Permission denied: '/etc/apt/source-file-1'",
),
(
"/etc/apt/source-file-2",
"/root/.cache/dest-file-2",
"[Errno 13] Permission denied: '/etc/apt/source-file-2'",
),
]
),
)

with pytest.raises(PopulateCacheDirError) as raised:
with AptCache() as apt_cache:
# set stage_cache directory so method does not return early
apt_cache.stage_cache = tmp_path
apt_cache._populate_stage_cache_dir()

assert mock_copytree.mock_calls == [call(Path("/etc/apt"), tmp_path / "etc/apt")]

# verify the data inside the shutil error was passed to PopulateCacheDirError
assert raised.value.get_details() == (
"Unable to copy /etc/apt/source-file-1 to /root/.cache/dest-file-1: "
"[Errno 13] Permission denied: '/etc/apt/source-file-1'\n"
"Unable to copy /etc/apt/source-file-2 to /root/.cache/dest-file-2: "
"[Errno 13] Permission denied: '/etc/apt/source-file-2'\n"
)


def test_populate_stage_cache_dir_permission_error(mocker, tmp_path):
"""Raise an error when the apt cache directory cannot be populated."""
mock_copytree = mocker.patch(
"snapcraft_legacy.internal.repo.apt_cache.shutil.copytree",
side_effect=PermissionError("[Errno 13] Permission denied: '/etc/apt"),
)

with pytest.raises(PopulateCacheDirError) as raised:
with AptCache() as apt_cache:
# set stage_cache directory so method does not return early
apt_cache.stage_cache = tmp_path
apt_cache._populate_stage_cache_dir()

assert mock_copytree.mock_calls == [call(Path("/etc/apt"), tmp_path / "etc/apt")]

# verify the data inside the permission error was passed to PopulateCacheDirError
assert raised.value.get_details() == (
f"Unable to copy {Path('/etc/apt')} to {tmp_path / 'etc/apt'}: "
"[Errno 13] Permission denied: '/etc/apt\n"
)
30 changes: 30 additions & 0 deletions tests/legacy/unit/repo/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,33 @@ def test_multiple_packages_not_found_error():
assert exception.get_details() is None
assert exception.get_docs_url() is None
assert exception.get_reportable() is False


def test_snapcraft_exception_handling():
exception = errors.PopulateCacheDirError(
[
(
"/etc/apt/source-file-1",
"/root/.cache/dest-file-1",
"[Errno 13] Permission denied: '/etc/apt/source-file-1'",
),
(
"/etc/apt/source-file-2",
"/root/.cache/dest-file-2",
"[Errno 13] Permission denied: '/etc/apt/source-file-2'",
),
]
)

assert exception.get_brief() == "Could not populate apt cache directory."
assert exception.get_resolution() == (
"Verify user has read access to contents of /etc/apt."
)
assert exception.get_details() == (
"Unable to copy /etc/apt/source-file-1 to /root/.cache/dest-file-1: "
"[Errno 13] Permission denied: '/etc/apt/source-file-1'\n"
"Unable to copy /etc/apt/source-file-2 to /root/.cache/dest-file-2: "
"[Errno 13] Permission denied: '/etc/apt/source-file-2'\n"
)
assert exception.get_docs_url() is None
assert exception.get_reportable() is False

0 comments on commit bfdacd2

Please sign in to comment.