Skip to content

Commit 2b541ca

Browse files
Merge pull request #3077 from fgallott/fixes-prototype
fix: lockfile builder stage handling, retry logic, and TMPDIR
2 parents 6ca4ca5 + 5af7e67 commit 2b541ca

4 files changed

Lines changed: 467 additions & 31 deletions

File tree

doozer/doozerlib/lockfile_prototype/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Constants and enums for the lockfile prototype package.
33
"""
44

5+
import re
56
from enum import Enum
67
from pathlib import Path
78

@@ -33,6 +34,8 @@
3334
# RPM pseudo-packages that appear in rpmdb but are not installable via DNF
3435
RPM_PSEUDO_PACKAGES = frozenset({"gpg-pubkey"})
3536

37+
VALID_PKG_NAME = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._+\-]*$")
38+
3639

3740
# rpm-lockfile-prototype stores extracted RPMDBs here. There is no env var
3841
# to override this path — only RPM_LOCKFILE_PROTOTYPE_DNF_CACHE controls

doozer/doozerlib/lockfile_prototype/generator.py

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ async def _resolve_all_stages(
436436
)
437437

438438
reinstall_pkgs: list[str] | None = None
439+
strippable: set[str] | None = None
439440
if stage_num == final_stage_num and not is_update_only and not has_bare_update:
440441
if image_pullspec:
441442
# --image mode: pass base image packages as reinstallPackages
@@ -451,6 +452,7 @@ async def _resolve_all_stages(
451452
base_pkgs = await self._get_base_image_packages(stage_num, image_pullspec, distgit_key)
452453
if base_pkgs:
453454
reinstall_pkgs = list(base_pkgs)
455+
strippable = set(base_pkgs) - set(packages) - set(upgrade_targets)
454456
self.logger.info(
455457
f"{distgit_key}: stage {stage_num}: {len(reinstall_pkgs)} base image "
456458
"packages will be reinstalled into lockfile"
@@ -463,7 +465,24 @@ async def _resolve_all_stages(
463465
if base_pkgs:
464466
extra = [p for p in base_pkgs if p not in packages]
465467
if extra:
468+
strippable = set(extra)
466469
packages = packages + extra
470+
elif stage_num != final_stage_num and not is_update_only:
471+
# Non-final stage (builder): reinstall the Dockerfile packages
472+
# so they appear in the lockfile even when already installed
473+
# on some architectures, and add base image packages to the
474+
# install list for conflict detection.
475+
if image_pullspec:
476+
reinstall_pkgs = list(packages)
477+
base_pkgs = await self._get_base_image_packages(stage_num, image_pullspec, distgit_key)
478+
if base_pkgs:
479+
extra = [p for p in base_pkgs if p not in packages]
480+
if extra:
481+
packages = packages + extra
482+
self.logger.info(
483+
f"{distgit_key}: stage {stage_num}: {len(extra)} base image "
484+
"packages added to install list for conflict detection"
485+
)
467486

468487
enable_only = [s.split("/")[0] for s in stage_info.module_specs] if stage_info.module_specs else None
469488

@@ -478,6 +497,7 @@ async def _resolve_all_stages(
478497
stage_num,
479498
module_enable=enable_only,
480499
reinstall_packages=reinstall_pkgs,
500+
strippable_packages=strippable,
481501
)
482502
if result:
483503
stage_lockfiles.append(result)
@@ -663,10 +683,18 @@ async def _resolve_stage_with_retry(
663683
stage_num: int,
664684
module_enable: list[str] | None = None,
665685
reinstall_packages: list[str] | None = None,
686+
strippable_packages: set[str] | None = None,
666687
) -> LockfileData | None:
667688
"""
668689
Resolve a single stage, retrying after removing unavailable packages.
669690
691+
Arg(s):
692+
strippable_packages (set[str] | None): Packages that may be
693+
silently removed during retries (e.g. base image packages
694+
added for conflict detection). If a missing package is NOT
695+
in this set, it is a required Dockerfile package and the
696+
error is raised immediately.
697+
670698
Return Value(s):
671699
LockfileData | None: Lockfile data, or None if all packages filtered out.
672700
"""
@@ -679,15 +707,23 @@ async def _resolve_stage_with_retry(
679707
# use the no-auth registry proxy instead.
680708
resolver_pullspec = ContainerImageHelper._proxy_pullspec(image_pullspec) if image_pullspec else None
681709

682-
# When reinstall_packages is set, also pass them as upgrade targets.
683-
# base.reinstall() raises PackagesNotAvailableError when the installed
684-
# version isn't in the configured repos — but rpm-lockfile-prototype
685-
# swallows that error when the package is also in upgradePackages
686-
# (the upgrade provides a replacement version).
710+
# When reinstall_packages comes from the base image (final stage),
711+
# also pass them as upgrade targets. base.reinstall() raises
712+
# PackagesNotAvailableError when the installed version isn't in
713+
# the configured repos — but rpm-lockfile-prototype swallows that
714+
# error when the package is also in upgradePackages (the upgrade
715+
# provides a replacement version).
716+
# For builder stages (strippable_packages is None), reinstall
717+
# packages are Dockerfile packages that may not be installed in
718+
# the base image — adding them to upgradePackages would cause
719+
# PackagesNotInstalledError.
687720
remaining_reinstall = list(reinstall_packages) if reinstall_packages else []
721+
promote_reinstall_to_upgrade = strippable_packages is not None
722+
retries_exhausted = False
688723

689-
for attempt in range(MAX_RESOLUTION_RETRIES):
690-
effective_upgrade = list(set(remaining_update_targets + remaining_reinstall)) if image_pullspec else None
724+
for _attempt in range(MAX_RESOLUTION_RETRIES):
725+
upgrade_extras = remaining_reinstall if promote_reinstall_to_upgrade else []
726+
effective_upgrade = list(set(remaining_update_targets + upgrade_extras)) if image_pullspec else None
691727
in_yaml = build_rpms_in_yaml(
692728
repo_list,
693729
arches,
@@ -699,11 +735,23 @@ async def _resolve_stage_with_retry(
699735
)
700736

701737
try:
738+
mode = "image" if resolver_pullspec else "bare"
739+
self.logger.info(
740+
f"{distgit_key}: stage {stage_num}: resolving {len(remaining_packages)} packages in {mode} mode"
741+
)
742+
self.logger.debug(f"{distgit_key}: stage {stage_num}: full package list: {remaining_packages}")
702743
return await self._resolver.resolve(in_yaml, image_pullspec=resolver_pullspec)
703744
except RuntimeError as e:
704745
missing = RpmResolver.parse_missing_packages(str(e))
705746
if not missing:
706747
raise
748+
if strippable_packages is not None:
749+
required_missing = missing - strippable_packages
750+
if required_missing:
751+
raise RuntimeError(
752+
f"{distgit_key}: stage {stage_num}: required packages not available "
753+
f"in configured repos: {sorted(required_missing)}"
754+
) from e
707755
prev_count = (
708756
len(remaining_packages)
709757
+ sum(len(v) for v in arch_pkgs.values())
@@ -733,7 +781,44 @@ async def _resolve_stage_with_retry(
733781
f"{distgit_key}: stage {stage_num}: no packages remaining after filtering, skipping"
734782
)
735783
return None
736-
raise RuntimeError(f"{distgit_key}: stage {stage_num}: exceeded {MAX_RESOLUTION_RETRIES} resolution retries")
784+
if strippable_packages is not None:
785+
required_reinstall = [p for p in remaining_reinstall if p not in strippable_packages]
786+
if not required_reinstall and remaining_reinstall:
787+
self.logger.info(
788+
f"{distgit_key}: stage {stage_num}: all {len(remaining_reinstall)} "
789+
"remaining reinstall packages are optional, skipping retries"
790+
)
791+
break
792+
else:
793+
retries_exhausted = True
794+
if strippable_packages is not None:
795+
required_pkgs = set(remaining_packages) - strippable_packages
796+
dropped = [p for p in remaining_reinstall if p not in required_pkgs]
797+
remaining_reinstall = [p for p in remaining_reinstall if p in required_pkgs]
798+
if retries_exhausted:
799+
self.logger.warning(
800+
f"{distgit_key}: stage {stage_num}: exceeded {MAX_RESOLUTION_RETRIES} retries, "
801+
f"dropped {len(dropped)} optional reinstall packages, "
802+
f"keeping {len(remaining_reinstall)} required"
803+
)
804+
else:
805+
self.logger.warning(
806+
f"{distgit_key}: stage {stage_num}: exceeded {MAX_RESOLUTION_RETRIES} retries, "
807+
"continuing without reinstall packages"
808+
)
809+
remaining_reinstall.clear()
810+
fallback_upgrade_extras = remaining_reinstall if promote_reinstall_to_upgrade else []
811+
effective_upgrade = list(set(remaining_update_targets + fallback_upgrade_extras)) if image_pullspec else None
812+
in_yaml = build_rpms_in_yaml(
813+
repo_list,
814+
arches,
815+
remaining_packages,
816+
arch_specific_packages=arch_pkgs,
817+
reinstall_packages=remaining_reinstall if image_pullspec else None,
818+
upgrade_packages=effective_upgrade,
819+
module_enable=module_enable,
820+
)
821+
return await self._resolver.resolve(in_yaml, image_pullspec=resolver_pullspec)
737822

738823
def _assemble_lockfile(self, stage_lockfiles: list[LockfileData], image_meta: ImageMetadata) -> LockfileData:
739824
"""
@@ -831,6 +916,7 @@ async def _resolve_with_reconciliation(
831916
stage_num: int,
832917
module_enable: list[str] | None = None,
833918
reinstall_packages: list[str] | None = None,
919+
strippable_packages: set[str] | None = None,
834920
) -> LockfileData | None:
835921
"""
836922
Resolve a stage with cross-arch version reconciliation.
@@ -851,6 +937,8 @@ async def _resolve_with_reconciliation(
851937
module_enable (list[str] | None): Module streams to enable.
852938
reinstall_packages (list[str] | None): Base image packages to
853939
reinstall from repos into the lockfile.
940+
strippable_packages (set[str] | None): Packages that may be
941+
silently removed during retries (conflict detection packages).
854942
Return Value(s):
855943
LockfileData | None: Resolved lockfile with consistent
856944
versions, or None if no packages remain.
@@ -866,6 +954,7 @@ async def _resolve_with_reconciliation(
866954
stage_num,
867955
module_enable=module_enable,
868956
reinstall_packages=reinstall_packages,
957+
strippable_packages=strippable_packages,
869958
)
870959
if not first_pass:
871960
return None
@@ -885,19 +974,25 @@ async def _resolve_with_reconciliation(
885974
)
886975

887976
pinned_packages = list(packages) + version_pins
977+
pinned_names = set(mismatches.keys())
978+
pinned_update_targets = [t for t in update_targets if t not in pinned_names]
979+
pinned_reinstall = (
980+
[p for p in reinstall_packages if p not in pinned_names] if reinstall_packages else reinstall_packages
981+
)
888982

889983
try:
890984
second_pass = await self._resolve_stage_with_retry(
891985
repo_list,
892986
arches,
893987
pinned_packages,
894988
arch_pkgs,
895-
update_targets,
989+
pinned_update_targets,
896990
image_pullspec,
897991
distgit_key,
898992
stage_num,
899993
module_enable=module_enable,
900-
reinstall_packages=reinstall_packages,
994+
reinstall_packages=pinned_reinstall,
995+
strippable_packages=strippable_packages,
901996
)
902997
except RuntimeError:
903998
self.logger.warning(

doozer/doozerlib/lockfile_prototype/resolver.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
RPMDB_CACHE_ERROR_PATTERNS,
2525
RPMDB_CACHE_PATH,
2626
SYSTEM_PYTHON,
27+
VALID_PKG_NAME,
2728
)
2829
from doozerlib.lockfile_prototype.models import LockfileData, RpmsInConfig
2930
from doozerlib.lockfile_prototype.utils import build_env
@@ -80,6 +81,7 @@ async def resolve(
8081

8182
env = build_env()
8283
env["RPM_LOCKFILE_PROTOTYPE_DNF_CACHE"] = self._cache_path
84+
env["TMPDIR"] = self._working_dir
8385
rc, _, stderr = await cmd_gather_async(cmd, check=False, env=env)
8486

8587
if rc != 0:
@@ -164,4 +166,4 @@ def parse_missing_packages(error_text: str) -> set[str]:
164166
m = re.search(r"No match for argument:\s*(\S+)", line.strip())
165167
if m:
166168
missing.add(m.group(1).strip().rstrip(":"))
167-
return missing
169+
return {p for p in missing if VALID_PKG_NAME.match(p)}

0 commit comments

Comments
 (0)