Skip to content

Commit

Permalink
partitions: add feature and validation (#489)
Browse files Browse the repository at this point in the history
Signed-off-by: Callahan Kovacs <[email protected]>
  • Loading branch information
mr-cal authored Jul 10, 2023
1 parent 553bbf7 commit d363fa7
Show file tree
Hide file tree
Showing 12 changed files with 696 additions and 6 deletions.
4 changes: 2 additions & 2 deletions craft_parts/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def __str__(self) -> str:
return "\n".join(components)


class FeatureDisabled(PartsError):
"""The requested feature is not enabled."""
class FeatureError(PartsError):
"""A feature is not configured as expected."""

def __init__(self, message: str) -> None:
self.message = message
Expand Down
7 changes: 6 additions & 1 deletion craft_parts/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@

@dataclasses.dataclass(frozen=True)
class Features(metaclass=Singleton):
"""Configurable craft-parts features."""
"""Configurable craft-parts features.
:cvar enable_overlay: Enables the overlay step.
:cvar enable_partitions: Enables the usage of partitions.
"""

enable_overlay: bool = False
enable_partitions: bool = False

@classmethod
def reset(cls) -> None:
Expand Down
9 changes: 9 additions & 0 deletions craft_parts/infos.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class ProjectVar(YamlModel):
updated: bool = False


# pylint: disable-next=too-many-instance-attributes
class ProjectInfo:
"""Project-level information containing project-specific fields.
Expand All @@ -67,6 +68,7 @@ class ProjectInfo:
:param project_vars: A dictionary containing the project variables.
:param custom_args: Any additional arguments defined by the application
when creating a :class:`LifecycleManager`.
:param partitions: A list of partitions.
"""

def __init__(
Expand All @@ -82,6 +84,7 @@ def __init__(
project_name: Optional[str] = None,
project_vars_part_name: Optional[str] = None,
project_vars: Optional[Dict[str, str]] = None,
partitions: Optional[List[str]] = None,
**custom_args: Any, # custom passthrough args
):
if not project_dirs:
Expand All @@ -99,6 +102,7 @@ def __init__(
self._project_name = project_name
self._project_vars_part_name = project_vars_part_name
self._project_vars = {k: ProjectVar(value=v) for k, v in pvars.items()}
self._partitions = partitions
self._custom_args = custom_args
self.global_environment: Dict[str, str] = {}

Expand Down Expand Up @@ -184,6 +188,11 @@ def project_options(self) -> Dict[str, Any]:
"project_vars": self._project_vars,
}

@property
def partitions(self) -> Optional[List[str]]:
"""Return the project's partitions."""
return self._partitions

def set_project_var(
self,
name: str,
Expand Down
44 changes: 43 additions & 1 deletion craft_parts/lifecycle_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class LifecycleManager:
:param project_vars_part_name: Project variables can only be set in the part
matching this name.
:param project_vars: A dictionary containing project variables.
:param partitions: A list of partitions to use when the partitions feature is
enabled. The first partition must be "default" and all partitions must be
lowercase alphabetical.
:param custom_args: Any additional arguments that will be passed directly
to :ref:`callbacks<callbacks>`.
"""
Expand All @@ -99,6 +102,7 @@ def __init__(
base_layer_hash: Optional[bytes] = None,
project_vars_part_name: Optional[str] = None,
project_vars: Optional[Dict[str, str]] = None,
partitions: Optional[List[str]] = None,
**custom_args: Any, # custom passthrough args
):
# pylint: disable=too-many-locals
Expand All @@ -115,6 +119,8 @@ def __init__(
if "parts" not in all_parts:
raise ValueError("parts definition is missing")

_validate_partitions(partitions)

packages.Repository.configure(application_package_name)

project_dirs = ProjectDirs(work_dir=work_dir)
Expand All @@ -130,6 +136,7 @@ def __init__(
project_dirs=project_dirs,
project_vars_part_name=project_vars_part_name,
project_vars=project_vars,
partitions=partitions,
**custom_args,
)

Expand Down Expand Up @@ -263,7 +270,7 @@ def get_primed_stage_packages(self, *, part_name: str) -> Optional[List[str]]:
def _ensure_overlay_supported() -> None:
"""Overlay is only supported in Linux and requires superuser privileges."""
if not Features().enable_overlay:
raise errors.FeatureDisabled("Overlays are not supported.")
raise errors.FeatureError("Overlays are not supported.")

if sys.platform != "linux":
raise errors.OverlayPlatformError()
Expand Down Expand Up @@ -324,3 +331,38 @@ def _build_part(
)

return part


def _validate_partitions(partitions: Optional[List[str]]) -> None:
"""Validate the partition feature set.
If the partition feature is enabled, then:
- the first partition must be "default"
- each partition must contain only lowercase alphabetical characters
- partitions are unique
:param partitions: Partition data to verify.
:raises ValueError: If the partitions are not valid or the feature is not enabled.
"""
if Features().enable_partitions:
if not partitions:
raise errors.FeatureError(
"Partition feature is enabled but no partitions are defined."
)

if partitions[0] != "default":
raise ValueError("First partition must be 'default'.")

if any(not re.fullmatch("[a-z]+", partition) for partition in partitions):
raise ValueError(
"Partitions must only contain lowercase alphabetical characters."
)

if len(partitions) != len(set(partitions)):
raise ValueError("Partitions must be unique.")

elif partitions:
raise errors.FeatureError(
"Partitions are defined but partition feature is not enabled."
)
2 changes: 1 addition & 1 deletion craft_parts/sequencer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def plan(
:returns: The list of actions that should be executed.
"""
if target_step == Step.OVERLAY and not Features().enable_overlay:
raise errors.FeatureDisabled("Overlay step is not supported.")
raise errors.FeatureError("Overlay step is not supported.")

self._actions = []
self._add_all_actions(target_step, part_names)
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,14 @@ def is_rtd() -> bool:
"codespell",
"coverage",
"isort",
"hypothesis",
"pydocstyle",
"pylint",
"pylint-fixme-info",
"pylint-pytest",
"pyright",
"pytest",
"pytest-check",
"pytest-cov",
"pytest-mock",
"requests-mock",
Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ def enable_overlay_feature():
Features.reset()


@pytest.fixture
def enable_partitions_feature():
assert Features().enable_partitions is False
Features.reset()
Features(enable_partitions=True)

yield

Features.reset()


@pytest.fixture(autouse=True)
def temp_xdg(tmpdir, mocker):
"""Use a temporary locaction for XDG directories."""
Expand Down
Loading

0 comments on commit d363fa7

Please sign in to comment.