mirror of
https://github.com/xcat2/xcat-dep.git
synced 2026-09-12 12:36:23 +00:00
feat(xcat-dep): manifest-driven apt repo completeness + signature gate, auto-run after assemble
Adds a real gate on the ASSEMBLED apt repo, per codename x arch, using debs-manifest.conf as the single source of truth, layered pure/testable: - BuildUtils: verify_repo_packages(\%expected,\%present) (MISSING/VERSION), verify_repo_signature(\%expected,\%observed) (UNSIGNED/WRONGKEY), and parse_packages_index($text) -- all PURE and unit-tested (happy+sad, no dpkg-deb). - sbuild-all.pl does the IO via one sub verify_assembled_repo: parses each published binary-<arch>/Packages (resolving arch-suffixed names like xcat-genesis-base-<arch>, reducing to upstream via deb_upstream_version to compare against the manifest pin), runs gpg --verify on each dists/<cn>/InRelease and extracts the signer fingerprint, then delegates to the two pure deciders and dies listing every [<cn>/<arch>] problem. - Runs AUTOMATICALLY at the end of assemble_apt (once Packages + signed Release exist); suppressible with --no-verify-repo; skipped under --dry-run. Also a standalone, lock-free, build-free '--verify-repo=<apt_dir>' mode using the script's --manifest/ --dists/--gpg-key-id/--gpg-home. Replaces the coarse pool-global hard-coded check. prove t/sbuild-all.t: 90/90 (was 71). Smoke-tested: complete tree passes; dropped pkg -> MISSING; wrong version -> VERSION; missing index -> MISSING-INDEX; all die nonzero. Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
+104
@@ -24,6 +24,7 @@ use MIME::Base64 qw(encode_base64);
|
||||
our @EXPORT_OK = qw(
|
||||
sh_quote print_step
|
||||
version_matches required_pkgs read_manifest standard_options
|
||||
verify_repo_packages verify_repo_signature parse_packages_index
|
||||
codename_to_version version_to_codename known_codenames
|
||||
chroot_name chroot_sources_list
|
||||
control_field genesis_deb_control
|
||||
@@ -139,6 +140,109 @@ sub standard_options {
|
||||
);
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Published-repo completeness gate (PURE decision + PURE index parsing; no I/O, no manifest parsing).
|
||||
# These are the unit-testable core of the manifest-driven completeness gate: the disk/name-resolution
|
||||
# layer (sbuild-all.pl) feeds them plain hashes/strings so the DECISION stays testable without a repo.
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
|
||||
# verify_repo_packages(\%expected, \%present) -> @problems
|
||||
# %expected : pkg name => manifest version pin ('*' = any)
|
||||
# %present : pkg name => the actual (upstream) version found in the repo, or undef/absent if not found
|
||||
# Returns human-readable problem strings (empty list = the repo is complete for this set):
|
||||
# "MISSING <pkg> (manifest requires <pin>)" -- required package not present at all
|
||||
# "VERSION <pkg>: repo has <got>, manifest pins <pin>" -- present but the pin is not satisfied
|
||||
# The pin semantics are exactly BuildUtils::version_matches (the same check validate_manifest uses on
|
||||
# freshly-built debs), so a repo that "validates on build" and a repo that "validates on publish" agree.
|
||||
# Pure: no I/O, deterministic ordering (problems returned in sorted package-name order).
|
||||
sub verify_repo_packages {
|
||||
my ($expected, $present) = @_;
|
||||
my @problems;
|
||||
for my $pkg (sort keys %$expected) {
|
||||
my $pin = $expected->{$pkg};
|
||||
my $got = $present->{$pkg};
|
||||
if (!defined $got) {
|
||||
push @problems, "MISSING $pkg (manifest requires $pin)";
|
||||
next;
|
||||
}
|
||||
push @problems, "VERSION $pkg: repo has $got, manifest pins $pin"
|
||||
unless version_matches($got, $pin);
|
||||
}
|
||||
return @problems;
|
||||
}
|
||||
|
||||
# verify_repo_signature(\%expected, \%observed) -> @problems
|
||||
# %expected : unit => the expected signing-key identity (for Ubuntu the unit is the codename:
|
||||
# { focal => <key>, jammy => <key>, ... })
|
||||
# %observed : unit => the key that ACTUALLY signed the unit's metadata (the string the IO layer
|
||||
# extracts from gpg), or undef/'' when the unit is unsigned / verification failed
|
||||
# Returns human-readable problem strings (empty list = every unit is signed by the expected key):
|
||||
# "UNSIGNED <unit> (expected <key>)" -- no valid signature at all
|
||||
# "WRONGKEY <unit>: signed by <observed>, expected <key>" -- signed, but by a different key
|
||||
# Pure: a string comparison only -- NO gpg call, no I/O (the IO layer runs gpg and passes %observed in).
|
||||
# Deterministic (problems returned in sorted unit order).
|
||||
sub verify_repo_signature {
|
||||
my ($expected, $observed) = @_;
|
||||
my @problems;
|
||||
for my $unit (sort keys %$expected) {
|
||||
my $exp = $expected->{$unit};
|
||||
my $obs = $observed->{$unit};
|
||||
if (!defined $obs || $obs eq '') {
|
||||
push @problems, "UNSIGNED $unit (expected $exp)";
|
||||
next;
|
||||
}
|
||||
push @problems, "WRONGKEY $unit: signed by $obs, expected $exp"
|
||||
unless $obs eq $exp;
|
||||
}
|
||||
return @problems;
|
||||
}
|
||||
|
||||
# _dpkg_available / _dpkg_ver_gt: a pure version-ORDERING oracle used only to break duplicate-stanza
|
||||
# ties in parse_packages_index. dpkg is asked to compare two version STRINGS (no repo/disk/manifest
|
||||
# I/O, deterministic); when the dpkg binary is absent the caller falls back to last-wins. Duplicate
|
||||
# package stanzas essentially never occur in an apt-ftparchive-generated index (one stanza per
|
||||
# package), so this path is defensive.
|
||||
my $DPKG_AVAILABLE;
|
||||
sub _dpkg_available {
|
||||
return $DPKG_AVAILABLE if defined $DPKG_AVAILABLE;
|
||||
$DPKG_AVAILABLE = (system('command -v dpkg >/dev/null 2>&1') == 0) ? 1 : 0;
|
||||
return $DPKG_AVAILABLE;
|
||||
}
|
||||
sub _dpkg_ver_gt {
|
||||
my ($a, $b) = @_;
|
||||
return system('dpkg', '--compare-versions', $a, 'gt', $b) == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
# parse_packages_index($text) -> \%{ package_name => version }
|
||||
# Parse a Debian 'Packages' index: RFC822 stanzas separated by blank line(s); each carries a
|
||||
# 'Package:' and a 'Version:'. Returns name => version (the FULL Debian version verbatim, epoch +
|
||||
# revision included -- the caller strips to the upstream part with deb_upstream_version). A stanza
|
||||
# lacking either field is skipped; malformed/empty input yields an empty hash.
|
||||
# DUP-NAME CHOICE: if a name appears in more than one stanza, keep the HIGHEST by dpkg version order
|
||||
# when dpkg is available, else LAST-WINS (the last stanza's version). Pure: no file/repo I/O (the only
|
||||
# subprocess is dpkg as a version-comparison oracle for the rare duplicate).
|
||||
sub parse_packages_index {
|
||||
my ($text) = @_;
|
||||
my %map;
|
||||
return \%map unless defined $text && $text ne '';
|
||||
for my $stanza (split /\n\n+/, $text) {
|
||||
next unless $stanza =~ /\S/;
|
||||
my ($name) = $stanza =~ /^Package:[ \t]*(\S+)/m;
|
||||
my ($ver) = $stanza =~ /^Version:[ \t]*(\S+)/m;
|
||||
next unless defined $name && defined $ver;
|
||||
if (exists $map{$name}) {
|
||||
if (_dpkg_available()) {
|
||||
$map{$name} = $ver if _dpkg_ver_gt($ver, $map{$name}); # keep the highest
|
||||
} else {
|
||||
$map{$name} = $ver; # last-wins fallback
|
||||
}
|
||||
} else {
|
||||
$map{$name} = $ver;
|
||||
}
|
||||
}
|
||||
return \%map;
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Chroot provisioning helpers (absorbed from mk-dep-chroots.sh; sbuild-all.pl auto-inits on first run).
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
|
||||
+199
-1
@@ -37,9 +37,10 @@ use Fcntl qw(:flock);
|
||||
use FindBin qw($RealBin);
|
||||
use lib $RealBin;
|
||||
use BuildUtils qw(sh_quote print_step version_matches required_pkgs read_manifest standard_options
|
||||
verify_repo_packages verify_repo_signature parse_packages_index
|
||||
codename_to_version known_codenames chroot_name chroot_sources_list
|
||||
control_field genesis_deb_control
|
||||
deb_field deb_version deb_hash cross_copy_genesis_deb);
|
||||
deb_field deb_version deb_upstream_version deb_hash cross_copy_genesis_deb);
|
||||
|
||||
my $script_dir = abs_path(dirname(__FILE__));
|
||||
my $repo_root = $script_dir;
|
||||
@@ -62,6 +63,12 @@ my $parallel_targets = 0;
|
||||
my ($skip_build, $skip_install, $skip_genesis, $skip_xcat_dep) = (0,0,0,0);
|
||||
my ($skip_createrepo, $skip_tarball) = (0,0);
|
||||
my $dry_run = 0;
|
||||
# Completeness+signature gate on the PUBLISHED apt index (what apt clients see). $verify_repo_arg set
|
||||
# (--verify-repo=<apt_dir>) runs the gate STANDALONE against that assembled apt dir and exits (no lock,
|
||||
# no build). $no_verify_repo suppresses the AUTOMATIC post-assembly gate that otherwise runs at the end
|
||||
# of assemble_apt. Default: automatic gate ON.
|
||||
my $verify_repo_arg = '';
|
||||
my $no_verify_repo = 0;
|
||||
my $gpg_sign = 0;
|
||||
my $gpg_key_id = 'xcat@megware.com';
|
||||
my $gpg_home = '';
|
||||
@@ -134,6 +141,8 @@ $spec{'genesis-deb=s'} = \@genesis_debs;
|
||||
$spec{'genesis-rpm=s'} = \$genesis_rpm;
|
||||
$spec{'genesis-rpm-ppc=s'} = \$genesis_rpm_ppc;
|
||||
$spec{'require-ppc-genesis!'} = \$require_ppc_genesis;
|
||||
$spec{'verify-repo=s'} = \$verify_repo_arg; # standalone gate: --verify-repo=<apt_dir>
|
||||
$spec{'no-verify-repo!'} = \$no_verify_repo; # suppress the automatic post-assembly gate
|
||||
$spec{'output=s'} = \$output_root; # --output alias
|
||||
$spec{'help|h'} = sub { pod2usage(-verbose => 1, -exitval => 0); };
|
||||
$spec{'man'} = sub { pod2usage(-verbose => 2, -exitval => 0); };
|
||||
@@ -177,6 +186,19 @@ my $snap_ts = strftime("%Y%m%d%H%M", gmtime($build_timestamp));
|
||||
$ENV{SOURCE_DATE_EPOCH} = $build_timestamp; # deterministic mtimes across the whole run
|
||||
|
||||
my %MANIFEST = read_manifest($manifest);
|
||||
|
||||
# Standalone gate: --verify-repo=<apt_dir> checks an already-assembled apt tree (completeness +
|
||||
# Release signatures) using THIS script's manifest resolution (--manifest or the default
|
||||
# debs-manifest.conf), --dists, and --gpg-key-id/--gpg-home, then exits. It takes NO run lock and does
|
||||
# NOT build. Dispatched here (after manifest + @dist_list are resolved) so it never trips the
|
||||
# build-only per-target section check below and never reaches the lock/build phases.
|
||||
if (length $verify_repo_arg) {
|
||||
die "FATAL: --verify-repo apt dir not found: $verify_repo_arg\n" unless -d $verify_repo_arg;
|
||||
print_step('Standalone repo verification (no build, no lock)');
|
||||
verify_assembled_repo(\%MANIFEST, abs_path($verify_repo_arg), \@dist_list);
|
||||
exit 0;
|
||||
}
|
||||
|
||||
for my $cn (@dist_list) {
|
||||
my $tgt = "$cn-$arch";
|
||||
die "FATAL: no manifest section [$tgt] in $manifest\n" unless $MANIFEST{$tgt};
|
||||
@@ -506,6 +528,150 @@ sub validate_manifest {
|
||||
print " all targets satisfy the manifest (packages present, version pins matched)\n";
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Phase: manifest-driven completeness + signature GATE on the PUBLISHED apt index (what apt clients
|
||||
# actually see). This replaces the coarse, pool-global, hard-coded Jenkinsfile check: the manifest is
|
||||
# the single source of truth and the assertion is made per codename x arch against the PUBLISHED
|
||||
# binary-<arch>/Packages index (not the staging pool). The pure DECISIONS (verify_repo_packages for
|
||||
# completeness, verify_repo_signature for the signer) and the index PARSING (parse_packages_index) live
|
||||
# in BuildUtils.pm and are unit-tested; this disk/IO layer only reads the index, resolves manifest names
|
||||
# to index keys, and drives gpg for the signature check.
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
|
||||
# repo_present_from_index($idx, @names): parse the PUBLISHED $idx (a binary-<arch>/Packages file) and
|
||||
# resolve each required manifest @names against it, returning %present = (reqname => upstream-version |
|
||||
# undef). Name-resolution mirrors deb_version/validate_manifest: try an EXACT index key first (most
|
||||
# packages -- ipmitool-xcat, goconserver, grub2-xcat, the Architecture:all boot bits keep their plain
|
||||
# names), else a key matching ^<name>- (the arch-suffixed xcat-genesis-base -> xcat-genesis-base-<arch>).
|
||||
# The published Version carries epoch+revision, so it is reduced to the UPSTREAM part (what the manifest
|
||||
# pins) via deb_upstream_version before it reaches the pure comparator.
|
||||
sub repo_present_from_index {
|
||||
my ($idx, @names) = @_;
|
||||
my %present = map { $_ => undef } @names;
|
||||
my $text = do { local $/; open my $fh, '<', $idx or die "FATAL: cannot read $idx: $!\n"; <$fh> };
|
||||
my $parsed = parse_packages_index($text);
|
||||
for my $name (@names) {
|
||||
my $full;
|
||||
if (exists $parsed->{$name}) {
|
||||
$full = $parsed->{$name}; # exact index key
|
||||
} else {
|
||||
my ($k) = sort grep { /^\Q$name\E-/ } keys %$parsed; # arch-suffixed (genesis)
|
||||
$full = $parsed->{$k} if defined $k;
|
||||
}
|
||||
$present{$name} = defined $full ? deb_upstream_version($full) : undef;
|
||||
}
|
||||
return %present;
|
||||
}
|
||||
|
||||
# resolve_expected_key(): the expected signing-key IDENTITY that --gpg-key-id names, resolved (via the
|
||||
# pipeline's GNUPGHOME) to the primary-key fingerprint so it can be compared against what gpg reports as
|
||||
# the actual signer. Falls back to the raw --gpg-key-id string when it cannot be resolved to a
|
||||
# fingerprint (then the gate degrades to presence-only -- see sig_observed_key).
|
||||
sub resolve_expected_key {
|
||||
my $g = $gpg_home ? "GNUPGHOME=" . sh_quote($gpg_home) . " " : '';
|
||||
my $out = `${g}gpg --list-keys --with-colons ${\ sh_quote($gpg_key_id)} 2>/dev/null`;
|
||||
for my $line (split /\n/, ($out // '')) {
|
||||
return $1 if $line =~ /^fpr:::::::::([0-9A-Fa-f]+):/; # first fpr = primary key fingerprint
|
||||
}
|
||||
return $gpg_key_id;
|
||||
}
|
||||
|
||||
# sig_observed_key($adir, $cn, $expected_key, $expected_is_fpr): run gpg --verify on the codename's
|
||||
# Release signature (clearsigned InRelease preferred, detached Release.gpg + Release fallback) and return
|
||||
# the key that ACTUALLY signed it -- undef when unsigned or verification FAILS, so verify_repo_signature
|
||||
# reports UNSIGNED. On success the primary-key fingerprint is extracted from the VALIDSIG status line
|
||||
# (GOODSIG keyid fallback); when a real fingerprint can be extracted AND the expected key resolved to a
|
||||
# fingerprint, that real fingerprint is returned so a mismatch surfaces as WRONGKEY. Otherwise the gate
|
||||
# degrades to presence-only (returns the expected key on success) rather than emit a spurious WRONGKEY.
|
||||
sub sig_observed_key {
|
||||
my ($adir, $cn, $expected_key, $expected_is_fpr) = @_;
|
||||
my $g = $gpg_home ? "GNUPGHOME=" . sh_quote($gpg_home) . " " : '';
|
||||
my $inrel = "$adir/dists/$cn/InRelease";
|
||||
my $rel = "$adir/dists/$cn/Release";
|
||||
my $relgpg = "$adir/dists/$cn/Release.gpg";
|
||||
my $cmd;
|
||||
if (-f $inrel) {
|
||||
$cmd = "${g}gpg --status-fd=1 --verify " . sh_quote($inrel) . " 2>/dev/null";
|
||||
} elsif (-f $relgpg && -f $rel) {
|
||||
$cmd = "${g}gpg --status-fd=1 --verify " . sh_quote($relgpg) . " " . sh_quote($rel) . " 2>/dev/null";
|
||||
} else {
|
||||
return undef; # no signature file at all -> UNSIGNED
|
||||
}
|
||||
my $out = `$cmd`;
|
||||
return undef if ($? >> 8) != 0; # gpg --verify FAILED -> treat as unsigned/bad
|
||||
my $obs_fpr = '';
|
||||
for my $line (split /\n/, ($out // '')) {
|
||||
if ($line =~ /^\[GNUPG:\]\s+VALIDSIG\s+(.*)$/) {
|
||||
my @f = split /\s+/, $1;
|
||||
$obs_fpr = $f[-1]; # last field = primary key fingerprint
|
||||
last;
|
||||
}
|
||||
}
|
||||
if ($obs_fpr eq '') {
|
||||
for my $line (split /\n/, ($out // '')) {
|
||||
if ($line =~ /^\[GNUPG:\]\s+GOODSIG\s+(\S+)/) { $obs_fpr = $1; last; }
|
||||
}
|
||||
}
|
||||
return ($expected_is_fpr && $obs_fpr ne '') ? $obs_fpr : $expected_key;
|
||||
}
|
||||
|
||||
# verify_assembled_repo($manifest_href, $apt_dir, $dists_aref): the ONE completeness+signature gate,
|
||||
# shared by the automatic post-assembly run (end of assemble_apt) and the standalone --verify-repo mode.
|
||||
# It is the IO layer: it PARSES the repository (parse_packages_index of each published
|
||||
# binary-<arch>/Packages -> %present; gpg --verify of each dists/<cn>/InRelease -> %observed signer),
|
||||
# PARSES the manifest (-> %expected pkg pins per cell) and resolves the GPG key (--gpg-key-id -> expected
|
||||
# signer), then delegates the DECISION to the pure verify_repo_packages (per codename x arch) and
|
||||
# verify_repo_signature (per codename). Package problems are [<cn>/<arch>]-prefixed; the pure signature
|
||||
# problems already carry the codename unit. Any problem dies non-zero.
|
||||
sub verify_assembled_repo {
|
||||
my ($man, $adir, $dists) = @_;
|
||||
my @all;
|
||||
my $sig_enabled = ($gpg_home ne '' || $gpg_sign);
|
||||
my $expected_key = $sig_enabled ? resolve_expected_key() : '';
|
||||
my $expected_is_fpr = ($expected_key =~ /^[0-9A-Fa-f]{16,}$/) ? 1 : 0;
|
||||
print " apt-dir: $adir\n";
|
||||
print " signature check: " . ($sig_enabled
|
||||
? "on (expected key $expected_key" . ($expected_is_fpr ? '' : ' [unresolved -> presence-only]') . ")"
|
||||
: "SKIPPED (no --gpg-home/--gpg-sign)") . "\n";
|
||||
|
||||
my (%exp_sig, %obs_sig);
|
||||
for my $cn (@$dists) {
|
||||
# completeness: manifest (source of truth) vs the PUBLISHED index, per codename x arch.
|
||||
for my $a (qw(amd64 ppc64el)) {
|
||||
my $tgt = "$cn-$a";
|
||||
my $req = $man->{$tgt};
|
||||
unless ($req && %$req) {
|
||||
print " [$cn/$a] no manifest section [$tgt] -- skipping (codename does not target this arch)\n";
|
||||
next;
|
||||
}
|
||||
my @names = required_pkgs([sort keys %$req], $skip_genesis, $skip_xcat_dep);
|
||||
my $idx = "$adir/dists/$cn/main/binary-$a/Packages";
|
||||
unless (-f $idx) {
|
||||
push @all, "[$cn/$a] MISSING-INDEX $cn/$a (no $idx)";
|
||||
next;
|
||||
}
|
||||
my %present = repo_present_from_index($idx, @names);
|
||||
my %pins = map { $_ => $req->{$_} } @names;
|
||||
push @all, map { "[$cn/$a] $_" } verify_repo_packages(\%pins, \%present);
|
||||
}
|
||||
# signature IO: record the expected + observed signer for this codename (decided in bulk below).
|
||||
if ($sig_enabled) {
|
||||
$exp_sig{$cn} = $expected_key;
|
||||
$obs_sig{$cn} = sig_observed_key($adir, $cn, $expected_key, $expected_is_fpr);
|
||||
}
|
||||
}
|
||||
push @all, verify_repo_signature(\%exp_sig, \%obs_sig) if $sig_enabled;
|
||||
|
||||
if (@all) {
|
||||
print "$_\n" for @all;
|
||||
die "FATAL: apt repo INCOMPLETE (" . scalar(@all) . " problem(s))\n";
|
||||
}
|
||||
print "[verify-repo] complete: all required packages present + version-pinned"
|
||||
. ($sig_enabled ? " + Release signatures valid (key $expected_key)" : "")
|
||||
. " for [" . join(' ', @$dists) . "] x {amd64,ppc64el}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
# Phase: assemble + sign the apt repo (absorbed build-apt-repo.sh; promote-on-success)
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
@@ -601,6 +767,13 @@ sub assemble_apt {
|
||||
if (-f $keysrc) { copy($keysrc, "$apt_dir/xcat-dep.asc"); }
|
||||
else { run("${g}gpg --armor --export " . sh_quote($gpg_key_id) . " > " . sh_quote("$apt_dir/xcat-dep.asc"), nofail => 1); }
|
||||
}
|
||||
# Automatic post-assembly GATE on the just-published index (completeness + Release signatures).
|
||||
# Runs once every codename's dists/<cn>/.../Packages + signed Release are written. Suppressed with
|
||||
# --no-verify-repo (iteration/debug); skipped under --dry-run (nothing was published).
|
||||
unless ($no_verify_repo || $dry_run) {
|
||||
print_step('Verify published apt repo (post-assembly completeness + signature gate)');
|
||||
verify_assembled_repo(\%MANIFEST, $apt_dir, \@dist_list);
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------
|
||||
@@ -702,6 +875,19 @@ Wipes+repopulates each codename's published C<pool>/C<dists> from validated stag
|
||||
C<binary-E<lt>archE<gt>> (Architecture:all packages land in every arch index) and gpg-signs
|
||||
C<Release>/C<InRelease>. Skipped with C<--skip-createrepo>.
|
||||
|
||||
=item Verify (published-repo gate)
|
||||
|
||||
After assembly, a manifest-driven gate asserts -- per codename E<times> arch, against the B<published>
|
||||
C<binary-E<lt>archE<gt>/Packages> index apt clients actually see (not the staging pool) -- that every
|
||||
manifest-required package is present at its pinned upstream version, and that each codename's
|
||||
C<Release> is validly gpg-signed by the expected key (C<InRelease>, or detached C<Release.gpg>). Any
|
||||
missing package, version mismatch, missing index, or unsigned/wrong-key signature fails the run. The
|
||||
pure decisions (C<BuildUtils::verify_repo_packages> for completeness, C<BuildUtils::verify_repo_signature>
|
||||
for the signer) and the index parsing (C<BuildUtils::parse_packages_index>) are unit-tested; this
|
||||
script's IO layer parses the repository + resolves the gpg key and feeds those pure deciders. This
|
||||
automatic gate is suppressed with C<--no-verify-repo>; the same check runs standalone against an
|
||||
already-assembled tree via C<--verify-repo=E<lt>apt_dirE<gt>>.
|
||||
|
||||
=item Tarball
|
||||
|
||||
A repo tarball build artifact (the deployable offline FRS dep bundle is produced by the pipeline's
|
||||
@@ -773,6 +959,18 @@ the default gives 8 concurrent build streams for a 4-codename matrix (4 per host
|
||||
|
||||
Skip the corresponding phase(s). C<--skip-build --skip-genesis> gives an assemble-only run.
|
||||
|
||||
=item B<--verify-repo> C<< =<apt_dir> >>
|
||||
|
||||
Standalone mode: verify an already-assembled apt tree at C<< <apt_dir> >> (completeness + Release
|
||||
signatures) using this script's manifest resolution (C<--manifest> or the default
|
||||
C<debs-manifest.conf>), C<--dists>, and C<--gpg-key-id>/C<--gpg-home>, then exit. Takes no run lock and
|
||||
builds nothing.
|
||||
|
||||
=item B<--no-verify-repo>
|
||||
|
||||
Suppress the B<automatic> post-assembly completeness+signature gate (for iteration/debug). The gate is
|
||||
ON by default.
|
||||
|
||||
=item B<--dry-run>
|
||||
|
||||
Print the planned actions without executing them.
|
||||
|
||||
@@ -13,6 +13,7 @@ use File::Temp qw(tempdir);
|
||||
use File::Path qw(make_path);
|
||||
use File::Basename qw(basename);
|
||||
use BuildUtils qw(required_pkgs version_matches read_manifest standard_options
|
||||
verify_repo_packages verify_repo_signature parse_packages_index
|
||||
codename_to_version version_to_codename known_codenames
|
||||
chroot_name chroot_sources_list
|
||||
control_field genesis_deb_control
|
||||
@@ -117,6 +118,88 @@ CTRL
|
||||
unlike($c, qr/^Replaces:/m, 'no Replaces invented when the maintained control is absent');
|
||||
}
|
||||
|
||||
# ---- verify_repo_packages: PURE completeness decision (no I/O; manifest = source of truth) -------
|
||||
{
|
||||
my %req = ('ipmitool-xcat' => '1.8.18', 'goconserver' => '0.3.3', 'xcat-genesis-base' => '*');
|
||||
|
||||
# happy: every required package present at a matching version (incl a '*' pin) -> no problems.
|
||||
my %ok = ('ipmitool-xcat' => '1.8.18', 'goconserver' => '0.3.3', 'xcat-genesis-base' => '2.18.0');
|
||||
is_deeply([verify_repo_packages(\%req, \%ok)], [],
|
||||
'all present + version-matching (incl * pin) -> no problems');
|
||||
|
||||
# missing: a required package absent -> exactly one MISSING problem.
|
||||
my %miss = ('ipmitool-xcat' => '1.8.18', 'xcat-genesis-base' => '2.18.0'); # goconserver absent
|
||||
my @m = verify_repo_packages(\%req, \%miss);
|
||||
is(scalar(@m), 1, 'a missing required package -> one problem');
|
||||
like($m[0], qr/^MISSING goconserver /, 'missing package -> "MISSING <pkg> ..."');
|
||||
|
||||
# version: present but the pin is not satisfied -> exactly one VERSION problem.
|
||||
my %ver = ('ipmitool-xcat' => '1.8.17', 'goconserver' => '0.3.3', 'xcat-genesis-base' => '2.18.0');
|
||||
my @v = verify_repo_packages(\%req, \%ver);
|
||||
is(scalar(@v), 1, 'a version-pin mismatch -> one problem');
|
||||
like($v[0], qr/^VERSION ipmitool-xcat: repo has 1\.8\.17, manifest pins 1\.8\.18$/,
|
||||
'mismatch -> "VERSION <pkg>: repo has <got>, manifest pins <pin>"');
|
||||
|
||||
# combined: one missing AND one mismatched -> two problems (one MISSING + one VERSION).
|
||||
my %both = ('ipmitool-xcat' => '1.8.17', 'xcat-genesis-base' => '2.18.0'); # goconserver absent too
|
||||
my @b = verify_repo_packages(\%req, \%both);
|
||||
is(scalar(@b), 2, 'combined missing + mismatch -> two problems');
|
||||
ok((grep { /^MISSING goconserver/ } @b), 'combined carries the MISSING problem');
|
||||
ok((grep { /^VERSION ipmitool-xcat/ } @b), 'combined carries the VERSION problem');
|
||||
}
|
||||
|
||||
# ---- verify_repo_signature: PURE signer decision (no gpg; IO layer passes %observed in) ----------
|
||||
{
|
||||
my $KEY = 'C55A3A47C780A856'; # the expected signing key identity (a unit)
|
||||
my %exp = (focal => $KEY, jammy => $KEY, noble => $KEY);
|
||||
|
||||
# happy: every codename signed by the expected key -> no problems.
|
||||
is_deeply([verify_repo_signature(\%exp, { focal => $KEY, jammy => $KEY, noble => $KEY })], [],
|
||||
'all codenames signed by the expected key -> no problems');
|
||||
|
||||
# unsigned: undef AND '' both count as unsigned/failed -> UNSIGNED.
|
||||
my @u = verify_repo_signature(\%exp, { focal => $KEY, jammy => undef, noble => '' });
|
||||
is(scalar(@u), 2, 'two unsigned/failed codenames -> two problems');
|
||||
ok((grep { $_ eq "UNSIGNED jammy (expected $KEY)" } @u), 'undef observed -> UNSIGNED <unit> (expected <key>)');
|
||||
ok((grep { $_ eq "UNSIGNED noble (expected $KEY)" } @u), "empty observed -> UNSIGNED <unit> (expected <key>)");
|
||||
|
||||
# wrongkey: signed, but by a different key -> WRONGKEY (no false UNSIGNED for the good ones).
|
||||
my @w = verify_repo_signature(\%exp, { focal => $KEY, jammy => 'DEADBEEFDEADBEEF', noble => $KEY });
|
||||
is_deeply(\@w, ["WRONGKEY jammy: signed by DEADBEEFDEADBEEF, expected $KEY"],
|
||||
'a different signer -> WRONGKEY <unit>: signed by <observed>, expected <key>');
|
||||
}
|
||||
|
||||
# ---- parse_packages_index: PURE apt 'Packages' index parser (name => full version) --------------
|
||||
{
|
||||
my $idx = <<'PKG';
|
||||
Package: ipmitool-xcat
|
||||
Version: 1.8.18-4snap202608101400
|
||||
Architecture: amd64
|
||||
Description: fixture
|
||||
|
||||
Package: xcat-genesis-base-amd64
|
||||
Version: 2.18.0-snap202608101400.5
|
||||
Architecture: all
|
||||
Description: genesis netboot image
|
||||
|
||||
Package: goconserver
|
||||
Version: 2:0.3.3-snap202608101400.57
|
||||
Architecture: amd64
|
||||
Description: fixture
|
||||
PKG
|
||||
my $m = parse_packages_index($idx);
|
||||
is($m->{'ipmitool-xcat'}, '1.8.18-4snap202608101400', 'first stanza name => version');
|
||||
is($m->{'xcat-genesis-base-amd64'}, '2.18.0-snap202608101400.5',
|
||||
'arch-suffixed genesis name parsed (across a blank-line separator)');
|
||||
is($m->{'goconserver'}, '2:0.3.3-snap202608101400.57', 'epoch version kept verbatim');
|
||||
is(scalar(keys %$m), 3, 'exactly the three stanzas parsed');
|
||||
|
||||
# malformed / empty input -> empty hash (a stanza lacking Package:/Version: is skipped).
|
||||
is_deeply(parse_packages_index(''), {}, 'empty input -> empty hash');
|
||||
is_deeply(parse_packages_index("garbage without fields\nno colon here\n"), {},
|
||||
'malformed input (no Package:/Version:) -> empty hash');
|
||||
}
|
||||
|
||||
# ---- deb_upstream_version: strip epoch + debian revision ----------------------------------------
|
||||
is(deb_upstream_version('1.8.18-4'), '1.8.18', 'strip -revision');
|
||||
is(deb_upstream_version('2:0.3.3-snap202608101400.57'), '0.3.3', 'strip epoch and -revision');
|
||||
|
||||
Reference in New Issue
Block a user