Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add "craftctl chroot" command #920

Draft
wants to merge 9 commits into
base: feature/pro-sources
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion craft_parts/ctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def run(cls, cmd: str, args: list[str]) -> str | None:

:raises RuntimeError: If the command is not handled.
"""
if cmd in ["default", "set"]:
if cmd in ["default", "set", "chroot"]:
_client(cmd, args)
return None

Expand Down
55 changes: 54 additions & 1 deletion craft_parts/executor/step_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,52 @@ def _process_api_commands(
message=f"invalid arguments to command {cmd_name!r}",
)
self._execute_builtin_handler(step)
elif cmd_name == "chroot":
if self._step_info.step != step.OVERLAY:
raise invalid_control_api_call(
message=f"{cmd_name!r} can only run in overlay step",
)
if len(cmd_args) < 1:
raise invalid_control_api_call(
message=(
f"invalid arguments to command {cmd_name!r} (want at least 1 argument)"
),
)

try:
target_dir = self._part.part_layer_dir
commands = [
f"mkdir -p {target_dir}/etc {target_dir}/dev {target_dir}/sys {target_dir}/proc {target_dir}/usr/share/ca-certificates {target_dir}/etc/apt {target_dir}/etc/ssl/certs",
f"cp /etc/resolv.conf {target_dir}/etc/",
f"mount --bind /dev {target_dir}/dev",
f"mount --bind /sys {target_dir}/sys",
f"mount --bind /proc {target_dir}/proc",
f"mount --bind /usr/share/ca-certificates/ {target_dir}/usr/share/ca-certificates",
f"mount --bind /etc/ssl/certs/ {target_dir}/etc/ssl/certs/",
f"mount --bind /etc/apt {target_dir}/etc/apt",
f"cp /etc/ca-certificates.conf {target_dir}/etc/",
" ".join(cmd_args),
f"umount {target_dir}/dev",
f"umount {target_dir}/sys",
f"umount {target_dir}/proc",
f"umount {target_dir}/etc/apt",
f"umount {target_dir}/usr/share/ca-certificates",
f"umount {target_dir}/etc/ssl/certs",
]
_create_and_run_script(
commands,
script_path=target_dir.absolute() / "overlay-chroot.sh",
cwd=target_dir,
stdout=self._stdout,
stderr=self._stderr,
with_root_access=True,
)
except (ValueError, RuntimeError) as err:
raise errors.InvalidControlAPICall(
part_name=self._part.name,
scriptlet_name=scriptlet_name,
message=str(err),
) from err
elif cmd_name == "set":
if len(cmd_args) != 1:
raise invalid_control_api_call(
Expand Down Expand Up @@ -443,6 +489,8 @@ def _create_and_run_script(
stdout: Stream,
stderr: Stream,
build_environment_script_path: Path | None = None,
*,
with_root_access: bool = False,
) -> None:
"""Create a script with step-specific commands and execute it."""
with script_path.open("w") as run_file:
Expand All @@ -460,4 +508,9 @@ def _create_and_run_script(
script_path.chmod(0o755)
logger.debug("Executing %r", script_path)

process.run([script_path], cwd=cwd, stdout=stdout, stderr=stderr, check=True)
if with_root_access and Path("/usr/bin/sudo").exists():
process.run(
["sudo", script_path], cwd=cwd, stdout=stdout, stderr=stderr, check=True
)
else:
process.run([script_path], cwd=cwd, stdout=stdout, stderr=stderr, check=True)
59 changes: 59 additions & 0 deletions tests/integration/lifecycle/test_craftctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,65 @@ def setup_fixture(new_dir, mocker):
mocker.patch("craft_parts.utils.os_utils.umount")


def test_craftctl_chroot(new_dir, partitions, capfd, mocker):
mocker.patch("craft_parts.lifecycle_manager._ensure_overlay_supported")
mocker.patch("craft_parts.overlays.OverlayManager.refresh_packages_list")

parts_yaml = textwrap.dedent(
"""\
parts:
foo:
plugin: dump
source: foo
override-pull: |
echo "pull step"
craftctl default
overlay-script: |
echo "overlay step"
craftctl chroot touch test.txt
"""
)
parts = yaml.safe_load(parts_yaml)

Path("foo").mkdir()
Path("foo/foo.txt").touch()

lf = craft_parts.LifecycleManager(
parts,
application_name="test_ctl",
cache_dir=new_dir,
base_layer_dir=new_dir,
base_layer_hash=b"hash",
partitions=partitions,
)

# Check if planning resulted in the correct list of actions.
actions = lf.plan(Step.OVERLAY)
assert actions == [
Action("foo", Step.PULL),
Action("foo", Step.OVERLAY),
]

# Execute each step and verify if scriptlet and built-in handler
# ran as expected.

with lf.action_executor() as ctx:
# Execute the pull step. The source file must have been
# copied to the part src directory.
ctx.execute(actions[0])
captured = capfd.readouterr()
assert captured.out == "pull step\n"

# Execute the overlay step and add a file to the overlay
# directory to track file migration.
ctx.execute(actions[1])
captured = capfd.readouterr()
assert Path("parts/foo/layer/test.txt").exists() is True
assert Path("parts/foo/layer/etc/resolv.conf").exists() is True
assert captured.out == "overlay step\n"
assert Path("overlay")


def test_craftctl_default(new_dir, partitions, capfd, mocker):
mocker.patch("craft_parts.lifecycle_manager._ensure_overlay_supported")
mocker.patch("craft_parts.overlays.OverlayManager.refresh_packages_list")
Expand Down
Loading