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

Support container provisioner in toolbox #3228

Merged
merged 7 commits into from
Jan 30, 2025
Merged
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
5 changes: 5 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ contrast to the :ref:`/spec/core/contact` key, this field is not
supposed to be updated and can be useful when trying to track down
the original author for consultation.

The ``container`` executor now works in `Fedora Toolbx`__ when Podman is run
using ``flatpak-spawn --host`` on the host system.

__ https://docs.fedoraproject.org/en-US/fedora-silverblue/toolbox/


tmt-1.41.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
24 changes: 24 additions & 0 deletions tests/provision/container/toolbox/main.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
summary: Test container provisioner in toolbox
description:
Verify that container provisioner works well when tmt is run from
a toolbox container and podman is run on the host system using
`flatpak-spawn --host`. This is a common setup used in Fedora
Silverblue.

require:
- toolbox
tag+:
- provision-only
- provision-container
require+:
- toolbox
adjust+:
- enabled: false
when: distro != fedora
because: Setting up toolbox on CS9 with default UBI9 toolbox image is a pain.
- check:
- how: avc
result: xfail
when: distro >= fedora-41
because: |
We are not interested in AVCs for this test due to complicated setup.
2 changes: 2 additions & 0 deletions tests/provision/container/toolbox/podman_wrapper
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
flatpak-spawn --host podman "$@"
84 changes: 84 additions & 0 deletions tests/provision/container/toolbox/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/bin/bash
. /usr/share/beakerlib/beakerlib.sh || exit 1

# Use `tmt try` to run this test locally, running directly the script will not work.

rlJournalStart
rlPhaseStartSetup
rlRun "toolbox_container_name=\$(uuidgen)" 0 "Generate toolbox container name"
rlRun "toolbox_user=toolbox" 0 "Set user for running toolbox"
rlPhaseEnd

rlPhaseStartTest "Create toolbox container"
# Add a toolbox user. Running toolbox under root user does not work well,
# so a separate user account is created.
rlRun "useradd $toolbox_user"
rlRun "toolbox_user_id=$(id -u $toolbox_user)"

# Make sure systemd user session runs for the new user. The user session
# hosts a dbus session, which is required for toolbox.
rlRun "loginctl enable-linger $toolbox_user"

# Add required environment variables for toolbox to the user's environment.
rlRun "echo export XDG_RUNTIME_DIR=/run/user/$toolbox_user_id >> /home/$toolbox_user/.bashrc"
rlRun "echo export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$toolbox_user_id/bus >> /home/$toolbox_user/.bashrc"

rlRun "sudo -iu $toolbox_user toolbox create -y $toolbox_container_name"
rlPhaseEnd

toolbox_run() {
local command="sudo -iu $toolbox_user toolbox run --container $toolbox_container_name $*"
echo "Command: $command"
eval "$command"
}

rlPhaseStartTest "Local execution via tmt: Install tmt from TMT_TREE"
TOOLBOX_TREE="/var/tmp/tree"
TMT_COMMAND="env -C ${TOOLBOX_TREE} hatch -e dev run env -C /tmp tmt"

rlRun "type toolbox_run"

# Install make and hatch
rlRun "toolbox_run sudo dnf -y install make hatch"

# Create a copy of the tmt tree, to mitigate possible permission issues
rlRun "cp -Rf ${TMT_TREE} ${TOOLBOX_TREE}"

# Copy tmt project into the toolbox container
rlRun "sudo -iu ${toolbox_user} podman cp ${TOOLBOX_TREE} $toolbox_container_name:${TOOLBOX_TREE}"

# Fix permissions for the toolbox user
rlRun "toolbox_run sudo chown -Rf ${toolbox_user}:${toolbox_user} ${TOOLBOX_TREE}"

# Initialize git in tmt tree, it is required for development installation
# and the tmt tree is not a git repository.
rlRun "toolbox_run git -C ${TOOLBOX_TREE} init"

# Install additional development dependencies
rlRun "toolbox_run make -C ${TOOLBOX_TREE} develop"
rlPhaseEnd

rlPhaseStartTest "Print tmt version installed in toolbox"
rlRun "toolbox_run $TMT_COMMAND --version"
rlPhaseEnd

rlPhaseStartTest "Add podman wrapper"
# Copy the wrapper from the toolbox user, the containers are local to the user.
# Need to use a copy of the wrapper, the TMT_TREE is a volume mount and thus
# it is not accessible to the toolbox user.
rlRun "cp podman_wrapper /tmp/podman_wrapper"
rlRun "sudo -iu ${toolbox_user} podman cp /tmp/podman_wrapper $toolbox_container_name:/usr/bin/podman"
rlRun "toolbox_run podman --version"
rlPhaseEnd

rlPhaseStartTest "Verify container provisioner works from toolbox"
rlRun RUNID="$(mktemp -u)"
rlRun -s "toolbox_run env -C /tmp ${TMT_COMMAND} run -i ${RUNID} -a -vvv provision -h container -i registry.fedoraproject.org/fedora:latest execute -h tmt -s \\\"echo hello from container\\\""
rlAssertGrep "content: hello from container" $rlRun_LOG
rlPhaseEnd

rlPhaseStartCleanup
rlRun "toolbox rm -f $toolbox_container_name" 0 "Remove toolbox container"
rlRun "userdel -rf toolbox"
rlPhaseEnd
rlJournalEnd
45 changes: 45 additions & 0 deletions tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ class GuestFacts(SerializableContainer):
has_selinux: Optional[bool] = None
is_superuser: Optional[bool] = None
is_ostree: Optional[bool] = None
is_toolbox: Optional[bool] = None
toolbox_container_name: Optional[str] = None

#: Various Linux capabilities and whether they are permitted to
#: commands executed on this guest.
Expand Down Expand Up @@ -589,6 +591,47 @@ def _query_is_ostree(self, guest: 'Guest') -> Optional[bool]:

return output.stdout.strip() == 'yes'

def _query_is_toolbox(self, guest: 'Guest') -> Optional[bool]:
# https://www.reddit.com/r/Fedora/comments/g6flgd/toolbox_specific_environment_variables/
output = self._execute(
guest,
Command(
tmt.utils.DEFAULT_SHELL,
'-c',
'if [ -e /run/.toolboxenv ]; then echo yes; else echo no; fi'))

if output is None or output.stdout is None:
return None

return output.stdout.strip() == 'yes'

def _query_toolbox_container_name(self, guest: 'Guest') -> Optional[str]:
output = self._execute(
guest,
Command(
tmt.utils.DEFAULT_SHELL,
'-c',
'if [ -e /run/.containerenv ]; then echo yes; else echo no; fi'))

if output is None or output.stdout is None:
return None

if output.stdout.strip() == 'no':
return None

output = self._execute(
guest,
Command('cat', '/run/.containerenv'))

if output is None or output.stdout is None:
return None

for line in output.stdout.splitlines():
if line.startswith('name="'):
return line[6:-1]

return None

def _query_capabilities(self, guest: 'Guest') -> dict[GuestCapability, bool]:
# TODO: there must be a canonical way of getting permitted capabilities.
# For now, we're interested in whether we can access kernel message buffer.
Expand All @@ -610,6 +653,8 @@ def sync(self, guest: 'Guest') -> None:
self.has_selinux = self._query_has_selinux(guest)
self.is_superuser = self._query_is_superuser(guest)
self.is_ostree = self._query_is_ostree(guest)
self.is_toolbox = self._query_is_toolbox(guest)
self.toolbox_container_name = self._query_toolbox_container_name(guest)
self.capabilities = self._query_capabilities(guest)

self.in_sync = True
Expand Down
16 changes: 14 additions & 2 deletions tmt/steps/provision/podman.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,22 @@ def push(
self._run_guest_command(Command(
"chcon", "--recursive", "--type=container_file_t", self.parent.plan.workdir
), shell=False, silent=True)

# In case explicit destination is given, use `podman cp` to copy data
# to the container
# to the container. If running in toolbox, make sure to copy from the toolbox
# container instead of localhost.
if source and destination:
self.podman(Command("cp", source, f"{self.container}:{destination}"))
container_name: Optional[str] = None
if self.parent.plan.my_run.runner.facts.is_toolbox:
container_name = self.parent.plan.my_run.runner.facts.toolbox_container_name
self.podman(
Command(
"cp",
f"{container_name}:{source}"
if container_name else source,
f"{self.container}:{destination}"
)
)

def pull(
self,
Expand Down