Skip to content
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
4 changes: 2 additions & 2 deletions doozer/doozerlib/lockfile_prototype/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
MAX_REINSTALL_STRIP_RETRIES = 5
DEFAULT_PLATFORM = "linux/amd64"

SYSTEM_PYTHON = "/usr/bin/python3"
RPM_LOCKFILE_ENTRY_POINT = "from rpm_lockfile import main; main()"
RPM_LOCKFILE_IMAGE = "default-route-openshift-image-registry.apps.artc2023.pc3z.p1.openshiftapps.com/art-cd/rpm-lockfile-prototype:v0.25.0"
CONTAINER_RPMDB_CACHE_PATH = Path("/root/.cache/rpm-lockfile-prototype/rpmdbs")

# Shell subshell expressions that evaluate to the current architecture
ARCH_SUBSHELL_KEYWORDS = ("$(arch)", "$(uname -m)", "$(uname -p)", "$(go env GOARCH)")
Expand Down
117 changes: 81 additions & 36 deletions doozer/doozerlib/lockfile_prototype/resolver.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
RPM resolution via rpm-lockfile-prototype subprocess.
RPM resolution via rpm-lockfile-prototype container.

Invokes the rpm-lockfile-prototype tool through system Python so
that python3-dnf (a system package built for the system Python) is
available. The venv Python may be a different minor version where
dnf cannot be imported.
Invokes the rpm-lockfile-prototype tool inside a podman container
so that python3-dnf and all system dependencies are self-contained.
The container image is pulled from the art-cluster internal registry
on first use if not already present locally.
"""

import logging
Expand All @@ -19,57 +19,113 @@
from artcommonlib.exectools import cmd_gather_async

from doozerlib.lockfile_prototype.constants import (
CONTAINER_RPMDB_CACHE_PATH,
DEFAULT_RPM_INFILE_NAME,
DEFAULT_RPM_LOCKFILE_NAME,
JENKINS_CACHE_DIR,
RPM_LOCKFILE_ENTRY_POINT,
RPM_LOCKFILE_IMAGE,
RPMDB_CACHE_ERROR_PATTERNS,
RPMDB_CACHE_SUBDIR,
SYSTEM_PYTHON,
VALID_PKG_NAME,
)
from doozerlib.lockfile_prototype.models import LockfileData, RpmsInConfig
from doozerlib.lockfile_prototype.utils import build_env


class RpmResolver:
"""
Invokes rpm-lockfile-prototype via system Python subprocess.
Invokes rpm-lockfile-prototype via podman container.

Maintains a persistent DNF repodata cache directory across
resolve() calls so that repeated runs against the same repos
(common during multi-image rebases) skip redundant downloads.
Pulls the container image from the art-cluster registry on
first use if not already present locally.
"""

def __init__(self, working_dir: Path, logger: logging.Logger | None = None, cache_dir: str | None = None):
def __init__(
self,
working_dir: Path | None = None,
logger: logging.Logger | None = None,
cache_dir: str | None = None,
image: str | None = None,
):
self.logger = logger or logutil.get_logger(__name__)
self._working_dir = str(working_dir)
self._working_dir = str(working_dir) if working_dir else None
self._cache_dir_owner = (
None if cache_dir else TemporaryDirectory(prefix="rpm-lockfile-cache-", dir=self._working_dir)
)
self._cache_path = cache_dir or self._cache_dir_owner.name
self._image = image or RPM_LOCKFILE_IMAGE

# In Jenkins, redirect the rpm-lockfile-prototype RPMDB cache to a
# persistent volume via XDG_CACHE_HOME so it survives across job runs
# and stays off the small root volume (~/.cache).
# Outside Jenkins, honour an existing XDG_CACHE_HOME if set.
# In Jenkins, redirect the RPMDB cache to a persistent volume so it
# survives across job runs and stays off the small root volume.
if os.environ.get("JENKINS_HOME"):
self._xdg_cache_home: Path | None = JENKINS_CACHE_DIR
xdg_cache_home: Path = JENKINS_CACHE_DIR
else:
self._xdg_cache_home = None
xdg_env = os.environ.get("XDG_CACHE_HOME")
cache_root = self._xdg_cache_home or (Path(xdg_env) if xdg_env else Path.home() / ".cache")
self._rpmdb_cache_path = cache_root / RPMDB_CACHE_SUBDIR
xdg_env = os.environ.get("XDG_CACHE_HOME")
xdg_cache_home = Path(xdg_env) if xdg_env else Path.home() / ".cache"
self._rpmdb_cache_path = xdg_cache_home / RPMDB_CACHE_SUBDIR
self.logger.info("RPMDB cache path: %s", self._rpmdb_cache_path)

def _build_podman_cmd(
self,
tmpdir: str,
image_pullspec: str | None,
) -> list[str]:
"""
Build the podman run command with volume mounts and env vars.

Arg(s):
tmpdir (str): Host temp directory with rpms.in.yaml.
image_pullspec (str | None): Base image for rpmdb context.
Return Value(s):
list[str]: Complete podman command.
"""
cmd = ["podman", "run", "--rm"]

# Work directory: rpms.in.yaml input and rpms.lock.yaml output
cmd.extend(["-v", f"{tmpdir}:/work:Z"])

# DNF repodata cache — :z (shared) so parallel resolve() calls can access
cmd.extend(["-v", f"{self._cache_path}:/cache:z"])
cmd.extend(["-e", "RPM_LOCKFILE_PROTOTYPE_DNF_CACHE=/cache"])

# RPMDB cache: host path → container /root/.cache/...
self._rpmdb_cache_path.mkdir(parents=True, exist_ok=True)
cmd.extend(["-v", f"{self._rpmdb_cache_path}:{CONTAINER_RPMDB_CACHE_PATH}:z"])

# Mount host entitlement certs for accessing protected repos.
# Use :ro without :z — SELinux won't allow relabeling system dirs.
for host_path in ("/etc/pki/entitlement", "/etc/rhsm/ca", "/etc/pki/rpm-gpg"):
if Path(host_path).is_dir():
cmd.extend(["-v", f"{host_path}:{host_path}:ro"])
Comment on lines +86 to +101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Work directory: rpms.in.yaml input and rpms.lock.yaml output
cmd.extend(["-v", f"{tmpdir}:/work:Z"])
# DNF repodata cache
cmd.extend(["-v", f"{self._cache_path}:/cache:Z"])
cmd.extend(["-e", "RPM_LOCKFILE_PROTOTYPE_DNF_CACHE=/cache"])
# RPMDB cache: host path → container /root/.cache/...
RPMDB_CACHE_PATH.mkdir(parents=True, exist_ok=True)
cmd.extend(["-v", f"{RPMDB_CACHE_PATH}:{CONTAINER_RPMDB_CACHE_PATH}:Z"])
# Mount host entitlement certs for accessing protected repos.
# Use :ro without :Z — SELinux won't allow relabeling system dirs.
for host_path in ("/etc/pki/entitlement", "/etc/rhsm/ca", "/etc/pki/rpm-gpg"):
if Path(host_path).is_dir():
cmd.extend(["-v", f"{host_path}:{host_path}:ro"])
# Work directory: rpms.in.yaml input and rpms.lock.yaml output
cmd.extend(["-v", f"{tmpdir}:/work:z"])
# DNF repodata cache
cmd.extend(["-v", f"{self._cache_path}:/cache:z"])
cmd.extend(["-e", "RPM_LOCKFILE_PROTOTYPE_DNF_CACHE=/cache"])
# RPMDB cache: host path → container /root/.cache/...
RPMDB_CACHE_PATH.mkdir(parents=True, exist_ok=True)
cmd.extend(["-v", f"{RPMDB_CACHE_PATH}:{CONTAINER_RPMDB_CACHE_PATH}:z"])
# Mount host entitlement certs for accessing protected repos.
# Use :ro without :z — SELinux won't allow relabeling system dirs.
for host_path in ("/etc/pki/entitlement", "/etc/rhsm/ca", "/etc/pki/rpm-gpg"):
if Path(host_path).is_dir():
cmd.extend(["-v", f"{host_path}:{host_path}:ro"])

I'm a bit rusty from my pre-ART days when I was doing similar with a container, but thought I needed z instead of Z for reasons (exclusive vs shared). We should evaluate what this looks like when running 2+ podman run simultaneously.


# Registry auth
auth_file = os.environ.get("QUAY_AUTH_FILE") or os.environ.get("REGISTRY_AUTH_FILE")
if auth_file:
cmd.extend(["-v", f"{auth_file}:/auth/auth.json:ro,z"])
cmd.extend(["-e", "REGISTRY_AUTH_FILE=/auth/auth.json"])

# Image name
cmd.append(self._image)

# Tool arguments
if image_pullspec:
cmd.extend(["--image", image_pullspec])
else:
cmd.append("--bare")
cmd.extend(["--outfile", "/work/" + DEFAULT_RPM_LOCKFILE_NAME, "/work/" + DEFAULT_RPM_INFILE_NAME])

return cmd

async def resolve(
self,
config: RpmsInConfig,
image_pullspec: str | None = None,
) -> LockfileData:
"""
Resolve RPM packages by running rpm-lockfile-prototype via
system Python as a subprocess.
Resolve RPM packages by running rpm-lockfile-prototype in a
podman container.

On RPMDB corruption errors, clears the cached RPMDB for the
image and retries once before raising.
Expand All @@ -87,25 +143,14 @@ async def resolve(

in_file.write_text(yaml.safe_dump(config.model_dump(exclude_none=True), sort_keys=False))

cmd = [SYSTEM_PYTHON, "-c", RPM_LOCKFILE_ENTRY_POINT]
if image_pullspec:
cmd.extend(["--image", image_pullspec])
else:
cmd.append("--bare")
cmd.extend(["--outfile", str(out_file), str(in_file)])

env = build_env()
env["RPM_LOCKFILE_PROTOTYPE_DNF_CACHE"] = self._cache_path
if self._xdg_cache_home:
env["XDG_CACHE_HOME"] = str(self._xdg_cache_home)
env["TMPDIR"] = self._working_dir
rc, _, stderr = await cmd_gather_async(cmd, check=False, env=env)
cmd = self._build_podman_cmd(tmpdir, image_pullspec)
rc, _, stderr = await cmd_gather_async(cmd, check=False)

if rc != 0:
if image_pullspec and self._is_rpmdb_corrupt(stderr):
self._clear_rpmdb_cache(image_pullspec)
self.logger.info("Retrying rpm-lockfile-prototype after RPMDB cache error")
rc, _, stderr = await cmd_gather_async(cmd, check=False, env=env)
rc, _, stderr = await cmd_gather_async(cmd, check=False)
if rc == 0:
return LockfileData.model_validate(yaml.safe_load(out_file.read_text()))
error_summary = stderr.strip().rsplit("\n", 1)[-1]
Expand Down
Loading
Loading