Make remote harvest zipapp stdlib-only
Lazy-load manifest, explain, and validation dependencies so the remote harvest zipapp does not require jsonschema, PyYAML, Paramiko, or other site-packages on the target host. Run the remote zipapp with Python isolated mode and site-packages disabled, while preserving existing CLI monkeypatch hooks.
This commit is contained in:
parent
70c90c62ae
commit
5932d22801
3 changed files with 94 additions and 3 deletions
|
|
@ -18,20 +18,38 @@ from .diff import (
|
||||||
post_webhook,
|
post_webhook,
|
||||||
send_email,
|
send_email,
|
||||||
)
|
)
|
||||||
from .explain import explain_state
|
|
||||||
from .harvest import harvest
|
from .harvest import harvest
|
||||||
from .harvest_safety import ensure_safe_output_parent, write_text_output_file
|
from .harvest_safety import ensure_safe_output_parent, write_text_output_file
|
||||||
from .manifest import manifest
|
|
||||||
from .remote import (
|
from .remote import (
|
||||||
remote_harvest,
|
remote_harvest,
|
||||||
RemoteSudoPasswordRequired,
|
RemoteSudoPasswordRequired,
|
||||||
RemoteSSHKeyPassphraseRequired,
|
RemoteSSHKeyPassphraseRequired,
|
||||||
)
|
)
|
||||||
from .sopsutil import SopsError, encrypt_file_binary
|
from .sopsutil import SopsError, encrypt_file_binary
|
||||||
from .validate import validate_harvest
|
|
||||||
from .version import get_enroll_version
|
from .version import get_enroll_version
|
||||||
|
|
||||||
|
|
||||||
|
def explain_state(*args, **kwargs):
|
||||||
|
"""Load the explain implementation only when that command is used."""
|
||||||
|
from .explain import explain_state as _explain_state
|
||||||
|
|
||||||
|
return _explain_state(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def manifest(*args, **kwargs):
|
||||||
|
"""Load manifest dependencies only when rendering a manifest."""
|
||||||
|
from .manifest import manifest as _manifest
|
||||||
|
|
||||||
|
return _manifest(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_harvest(*args, **kwargs):
|
||||||
|
"""Load jsonschema only when validation is requested."""
|
||||||
|
from .validate import validate_harvest as _validate_harvest
|
||||||
|
|
||||||
|
return _validate_harvest(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _discover_config_path(argv: list[str]) -> Optional[Path]:
|
def _discover_config_path(argv: list[str]) -> Optional[Path]:
|
||||||
"""Return the config path to use, if any.
|
"""Return the config path to use, if any.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1126,6 +1126,8 @@ def _remote_harvest(
|
||||||
# Run remote harvest.
|
# Run remote harvest.
|
||||||
argv: list[str] = [
|
argv: list[str] = [
|
||||||
remote_python,
|
remote_python,
|
||||||
|
"-I",
|
||||||
|
"-S",
|
||||||
rapp,
|
rapp,
|
||||||
"harvest",
|
"harvest",
|
||||||
"--out",
|
"--out",
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,9 @@ def test_remote_harvest_happy_path(tmp_path: Path, monkeypatch):
|
||||||
assert promote_argv.count("enroll.pyz") >= 2
|
assert promote_argv.count("enroll.pyz") >= 2
|
||||||
assert "/tmp/enroll-root-123/enroll.pyz" in harvest_cmd
|
assert "/tmp/enroll-root-123/enroll.pyz" in harvest_cmd
|
||||||
assert "/tmp/enroll-remote-123/enroll.pyz" not in harvest_cmd
|
assert "/tmp/enroll-remote-123/enroll.pyz" not in harvest_cmd
|
||||||
|
harvest_argv = shlex.split(harvest_cmd)
|
||||||
|
pyz_i = harvest_argv.index("/tmp/enroll-root-123/enroll.pyz")
|
||||||
|
assert harvest_argv[pyz_i - 2 : pyz_i] == ["-I", "-S"]
|
||||||
assert not any(_is_pyz_verify_cmd(c) for c, _pty in calls)
|
assert not any(_is_pyz_verify_cmd(c) for c, _pty in calls)
|
||||||
|
|
||||||
# The trusted digest must be obtained while the archive is still private,
|
# The trusted digest must be obtained while the archive is still private,
|
||||||
|
|
@ -1251,6 +1254,74 @@ def test_remote_verify_pyz_sha256_rejects_nonzero_rc():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_enroll_pyz_harvest_starts_without_site_packages(tmp_path: Path):
|
||||||
|
"""The remote payload must not depend on packages installed on the target.
|
||||||
|
|
||||||
|
``-S`` disables site-packages, reproducing a target that has Python but no
|
||||||
|
jsonschema/PyYAML. ``-I`` also ignores PYTHON* environment variables and
|
||||||
|
the current directory. The old eager CLI imports failed here before
|
||||||
|
argparse could dispatch to ``harvest``.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
pyz, _sha = r._build_enroll_pyz(tmp_path)
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, "-I", "-S", str(pyz), "harvest", "--help"],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
assert "usage: enroll harvest" in proc.stdout
|
||||||
|
assert "ModuleNotFoundError" not in proc.stderr
|
||||||
|
assert "jsonschema" not in proc.stderr
|
||||||
|
assert "yaml" not in proc.stderr.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_enroll_pyz_runs_harvest_without_site_packages(tmp_path: Path):
|
||||||
|
"""Exercise command dispatch and the actual harvest implementation under -S."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
pyz, _sha = r._build_enroll_pyz(build_dir)
|
||||||
|
out_dir = tmp_path / "bundle"
|
||||||
|
proc = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-I",
|
||||||
|
"-S",
|
||||||
|
str(pyz),
|
||||||
|
"harvest",
|
||||||
|
"--out",
|
||||||
|
str(out_dir),
|
||||||
|
"--exclude-path",
|
||||||
|
"/**",
|
||||||
|
"--assume-safe-path",
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
assert (out_dir / "state.json").is_file()
|
||||||
|
assert "ModuleNotFoundError" not in proc.stderr
|
||||||
|
assert "jsonschema" not in proc.stderr
|
||||||
|
assert "yaml" not in proc.stderr.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_build_enroll_pyz_excludes_tests_and_caches_and_returns_sha(tmp_path: Path):
|
def test_build_enroll_pyz_excludes_tests_and_caches_and_returns_sha(tmp_path: Path):
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue