diff --git a/tests/prepare/bootc-large-env/data/.fmf/version b/tests/prepare/bootc-large-env/data/.fmf/version new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/tests/prepare/bootc-large-env/data/.fmf/version @@ -0,0 +1 @@ +1 diff --git a/tests/prepare/bootc-large-env/data/plans.fmf b/tests/prepare/bootc-large-env/data/plans.fmf new file mode 100644 index 0000000000..75810040d5 --- /dev/null +++ b/tests/prepare/bootc-large-env/data/plans.fmf @@ -0,0 +1,19 @@ +discover: + how: fmf + +provision: + how: virtual + +execute: + how: tmt + +/centos-stream-10: + provision+: + image: https://artifacts.dev.testing-farm.io/images/CentOS-Stream-10-image-mode-x86_64.qcow2 + + /large-env: + summary: "Prepare/shell with large environment on bootc guest" + prepare: + how: shell + script: + - dnf -y install tree diff --git a/tests/prepare/bootc-large-env/data/test.fmf b/tests/prepare/bootc-large-env/data/test.fmf new file mode 100644 index 0000000000..91d66a578e --- /dev/null +++ b/tests/prepare/bootc-large-env/data/test.fmf @@ -0,0 +1,4 @@ +summary: Verify prepare/shell with large environment succeeded +test: | + tree --version + bootc status --booted --format humanreadable diff --git a/tests/prepare/bootc-large-env/main.fmf b/tests/prepare/bootc-large-env/main.fmf new file mode 100644 index 0000000000..815a0e367a --- /dev/null +++ b/tests/prepare/bootc-large-env/main.fmf @@ -0,0 +1,15 @@ +summary: Test bootc image-mode with large plan environment +description: | + Verify that prepare/shell on bootc image-mode guests succeeds + when plan environment contains large values (e.g. base64-encoded + secrets). Without the push() fix, the generated Containerfile + was passed as a cat heredoc SSH command-line argument, exceeding + the OS ARG_MAX limit. +link: + - verifies: https://github.com/teemtee/tmt/issues/4518 +tag+: + - provision-only + - provision-virtual +require: + - tmt+provision-virtual +duration: 2h diff --git a/tests/prepare/bootc-large-env/test.sh b/tests/prepare/bootc-large-env/test.sh new file mode 100755 index 0000000000..3b983e64ed --- /dev/null +++ b/tests/prepare/bootc-large-env/test.sh @@ -0,0 +1,41 @@ +#!/bin/bash +. /usr/share/beakerlib/beakerlib.sh || exit 1 + +rlJournalStart + rlPhaseStartSetup + rlRun "pushd data" + rlRun "run=\$(mktemp -d --tmpdir=/var/tmp/tmt)" 0 "Create run directory" + + # Generate a ~67KB base64 string simulating an encoded secret. + # This exceeds ARG_MAX when multiplied across Containerfile RUN + # directives (the bug), but is small enough to pass through + # individual SSH execute() calls. + rlRun "python3 -c 'import base64; print(\"LARGE_SECRET: \" + base64.b64encode(b\"x\" * 50000).decode())' > large-env.yaml" + rlRun "echo \"LARGE_SECRET size: \$(wc -c < large-env.yaml) bytes\"" + rlPhaseEnd + + rlPhaseStartTest "Prepare/shell with large environment on bootc guest" + rlRun -s "tmt -dddvvv run --scratch -i \$run --environment-file large-env.yaml plan --name /plans/centos-stream-10/large-env" + + # Verify the prepare/shell command was collected + rlAssertGrep "Collected command for Containerfile" \$rlRun_LOG + + # Verify the container image was built + rlAssertGrep "building container image" \$rlRun_LOG + + # Verify bootc switch was called + rlAssertGrep "switching to new image" \$rlRun_LOG + + # Verify reboot happened + rlAssertGrep "rebooting to apply new image" \$rlRun_LOG + + # Verify tree --version ran successfully in the test + rlAssertGrep "tree v" \$rlRun_LOG + rlPhaseEnd + + rlPhaseStartCleanup + rlRun "rm -rf \$run" 0 "Remove run directory" + rlRun "rm -f large-env.yaml" 0 "Remove generated environment file" + rlRun "popd" + rlPhaseEnd +rlJournalEnd diff --git a/tmt/guest/__init__.py b/tmt/guest/__init__.py index a85111e87a..eedbb3a5e6 100644 --- a/tmt/guest/__init__.py +++ b/tmt/guest/__init__.py @@ -1797,7 +1797,7 @@ def package_manager( ) return tmt.package_managers.find_package_manager(self.facts.package_manager)( - guest=self, logger=self._logger + guest=self, parent=self, logger=self._logger ) @functools.cached_property @@ -1808,7 +1808,7 @@ def bootc_builder( raise tmt.utils.GeneralError(f"Bootc builder was not detected on guest '{self.name}'.") return tmt.package_managers.find_package_manager(self.facts.bootc_builder)( - guest=self, logger=self._logger + guest=self, parent=self, logger=self._logger ) @functools.cached_property diff --git a/tmt/package_managers/__init__.py b/tmt/package_managers/__init__.py index 2ef2ca8f44..94f4031f45 100644 --- a/tmt/package_managers/__init__.py +++ b/tmt/package_managers/__init__.py @@ -308,8 +308,14 @@ class PackageManagerEngine(tmt.utils.Common): command: Command options: Command - def __init__(self, *, guest: 'Guest', logger: tmt.log.Logger) -> None: - super().__init__(logger=logger) + def __init__( + self, + *, + guest: 'Guest', + parent: Optional['tmt.utils.Common'] = None, + logger: tmt.log.Logger, + ) -> None: + super().__init__(parent=parent, logger=logger) self.guest = guest @@ -439,10 +445,16 @@ class PackageManager(tmt.utils.Common, Generic[PackageManagerEngineT]): #: may be installed togethers, and therefore a priority is needed. probe_priority: int = 0 - def __init__(self, *, guest: 'Guest', logger: tmt.log.Logger) -> None: - super().__init__(logger=logger) - - self.engine = self._engine_class(guest=guest, logger=logger) + def __init__( + self, + *, + guest: 'Guest', + parent: Optional['tmt.utils.Common'] = None, + logger: tmt.log.Logger, + ) -> None: + super().__init__(parent=parent, logger=logger) + + self.engine = self._engine_class(guest=guest, parent=parent, logger=logger) self.guest = guest diff --git a/tmt/package_managers/bootc.py b/tmt/package_managers/bootc.py index b2287dc718..570f78b7f4 100644 --- a/tmt/package_managers/bootc.py +++ b/tmt/package_managers/bootc.py @@ -5,6 +5,7 @@ import tmt.utils from tmt.container import PYDANTIC_V1, ConfigDict, MetadataContainer +from tmt.guest import TransferOptions from tmt.package_managers import ( Installable, Options, @@ -258,8 +259,18 @@ def build_container(self) -> Optional[CommandOutput]: ')"' ) ) - self.guest.execute( - ShellScript(f'cat < {containerfile_path!s} \n{containerfile} \nEOF') + # Write Containerfile via push() to avoid exceeding + # the OS ARG_MAX limit on the SSH command line. + assert self.workdir is not None + local_containerfile = self.workdir / 'Containerfile' + local_containerfile.write_text(containerfile) + self.guest.push( + source=local_containerfile, + destination=containerfile_path, + options=TransferOptions( + recursive=False, + compress=True, + ), ) self.debug(f"containerfile content: {containerfile}")