mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-08 21:14:56 +09:00
fix(release): compare host-less image refs on digest alone (#3905)
Fixes the false `promotion changed the container repository/digest set` that blocks #3904. `normalized_refs()` in `hack/verify-promoted-packages.sh` gives a host-less ref a repository equal to its own tag. `hack/lib/image-refs.sh` emits two entries per shape-3 map, the correctly joined ref plus shape 1's recursive scrape of the bare `tag: <tag>@sha256:...` scalar, and on that bare scalar `repo="${image%:*}"` is a no-op because there is no `/` and no `:` in it. Promotion rewrites exactly those tags, so `v1.6.2-rc.1` to `v1.6.2` shows up as six changed repositories whose digests are identical on both sides. `promote-retag.sh` never hits this because it drops host-less refs through its ownership filter, the verifier had no equivalent. Host-less refs now compare on digest alone, which is the whole of the container identity such a ref carries. Checked against the real published artifacts: before the fix 61 entries each side differing by those six pairs, after it 60 each side with the sets equal and 48 container digests identical. Two things this deliberately does not do, both written as comments at the fix. Not dropping host-less refs outright, because `packages/system/kuberture/values.yaml` carries an `image:` map with a `tag:` and no `repository:`, so shape 1 is the only rule that ever sees its digest and a drop would silently stop proving it unchanged. And not reusing `promote-retag.sh`'s `${REGISTRY}/` ownership filter, because the verify job's `REGISTRY` names the private build registry while both artifacts under comparison live on the public one, so that filter drops all 48 refs, and the emptiness guard above tests the raw collection rather than the filtered set, so the check would pass by comparing two empty sets. Second commit adds the fixture that was missing. The existing suite writes a host-bearing string and exercises a passing rc to stable rewrite, so it read as coverage while never building a shape-3 split map, which the library itself calls the dominant shape. Reverting the fix reddens the new case on the digest-set assertion, and replacing the fix with an outright skip of host-less refs makes the verifier exit 0 on a genuinely changed digest. Once this is on `release-1.6` the check on #3904 re-runs against this copy, so no `rc.2` re-cut is needed. `main` carries the same script and wants the same fix.
This commit is contained in:
2 files changed
+143
-1
No files matched your search
@@ -206,7 +206,44 @@ normalized_refs() {
|
||||
digest="${raw##*@}"
|
||||
image="${without_digest##*/}"
|
||||
if [ "$without_digest" = "$image" ]; then
|
||||
repo="${image%:*}"
|
||||
# No `/` anywhere ahead of the digest, so nothing in this ref names a
|
||||
# registry host or a repository path — it is a bare scalar sitting in
|
||||
# front of a digest, and the collector emits those by design. Shape 3
|
||||
# (`repository:` on one key, `tag: <tag>@sha256:<digest>` on another) is
|
||||
# the dominant shape in the tree, and shape 1's recursive descent sees
|
||||
# that `tag` scalar on its own, so every shape-3 map arrives here twice:
|
||||
# once correctly joined, once as the bare tag. hack/lib/image-refs.sh
|
||||
# documents that degeneracy and leaves host-less refs to its callers —
|
||||
# promote-retag.sh drops them through its ownership filter, and this
|
||||
# function had no equivalent. `${image%:*}` is then a no-op (a tag holds
|
||||
# no colon) and the TAG arrives as the repository. Promotion rewrites
|
||||
# exactly those tags, which is how a promotion that moved no container
|
||||
# bytes at all reported six changed "repositories" whose digests were
|
||||
# identical on both sides.
|
||||
#
|
||||
# Compare such a ref on its digest alone. The digest is the whole of the
|
||||
# container identity a host-less ref carries, and it is the only part
|
||||
# promotion must not move; the empty repository yields a leading `@`,
|
||||
# which no real repository name can produce, so these cannot collide with
|
||||
# a host-bearing entry. Where a repository name does exist behind one of
|
||||
# them — kube-ovn's `global.images.kubeovn`, whose host lives in
|
||||
# `global.registry.address` — the same image also arrives host-bearing
|
||||
# from shape 4, so a repository change stays visible through that entry.
|
||||
#
|
||||
# Dropping host-less refs outright would be wrong rather than merely
|
||||
# blunter: packages/system/kuberture/values.yaml carries an `image:` map
|
||||
# with a `tag:` and no `repository:`, so shape 1 is the only rule that
|
||||
# ever sees its digest. Skipping it would silently stop proving that
|
||||
# digest is unchanged — a hole in the very guard this branch repairs.
|
||||
#
|
||||
# Do NOT reach for promote-retag.sh's `${REGISTRY}/` ownership filter to
|
||||
# do this job. The verify job's REGISTRY names the private build registry
|
||||
# while both artifacts under comparison live on the public one, so that
|
||||
# filter drops every ref; and the emptiness guard above tests the RAW
|
||||
# collection rather than the filtered set, so the proof would then pass
|
||||
# by comparing two empty sets — failing open on the one invariant it
|
||||
# exists to establish.
|
||||
repo=""
|
||||
else
|
||||
repo="${without_digest%/*}/${image%:*}"
|
||||
fi
|
||||
|
||||
@@ -365,3 +365,108 @@ EOF
|
||||
[ "$rc" -ne 0 ]
|
||||
grep -q 'promotion changed the container repository/digest set' "$tmp/err"
|
||||
}
|
||||
|
||||
# Shape 3 — `repository:` on one key, `tag: <tag>@sha256:<digest>` on another —
|
||||
# is the shape hack/lib/image-refs.sh itself calls the dominant one, and the one
|
||||
# no fixture above builds. The collector emits TWO entries for such a map: the
|
||||
# correctly joined repository@digest, plus shape 1's recursive scrape of the
|
||||
# bare `tag` scalar, which names no repository at all. Promotion rewrites
|
||||
# exactly that scalar, so normalization keeping it — treating the tag as the
|
||||
# repository — makes every clean promotion look as though it moved container
|
||||
# bytes. The single-string ref in _make_verify_fixture cannot reach that path:
|
||||
# it carries a registry host, so it normalizes through the other branch and its
|
||||
# own rc-to-stable rewrite passes either way. That is why the suite looked
|
||||
# covered while the dominant shape went untested.
|
||||
_write_split_map() {
|
||||
cat > "$1" <<EOF
|
||||
linstorCSI:
|
||||
image:
|
||||
repository: ghcr.io/cozystack/cozystack/linstor-csi
|
||||
tag: $2@sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
|
||||
EOF
|
||||
}
|
||||
|
||||
# Same repository and the same digest on every side. ONLY the tag string moves,
|
||||
# which is exactly and only what promotion is meant to change.
|
||||
_add_split_map_package() {
|
||||
t="$1"
|
||||
mkdir -p "$t/release/system/linstor" "$t/artifact/system/linstor" \
|
||||
"$t/rc-artifact/system/linstor"
|
||||
_write_split_map "$t/release/system/linstor/values.yaml" v9.9.9
|
||||
_write_split_map "$t/artifact/system/linstor/values.yaml" v9.9.9
|
||||
_write_split_map "$t/rc-artifact/system/linstor/values.yaml" v9.9.9-rc.3
|
||||
}
|
||||
|
||||
@test "accepts a split-map tag rewritten from rc to stable" {
|
||||
tmp="$(_test_workspace)/fixture"
|
||||
_make_verify_fixture "$tmp"
|
||||
_add_split_map_package "$tmp"
|
||||
|
||||
rc=0
|
||||
MOCK_ARTIFACT="$tmp/artifact" MOCK_RC_ARTIFACT="$tmp/rc-artifact" MOCK_FLUX_LOG="$tmp/flux.log" \
|
||||
VERIFY_PACKAGES_WORKDIR="$tmp/work" \
|
||||
PATH="$tmp/bin:$PATH" \
|
||||
hack/verify-promoted-packages.sh 9.9.9 "$tmp/release" \
|
||||
> "$tmp/out" 2> "$tmp/err" || rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "verification exited $rc" >&2
|
||||
cat "$tmp/err" >&2
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
grep -q "stable tags, identical package tree, and the rc artifact's container digests" "$tmp/out"
|
||||
}
|
||||
|
||||
# The property a blunter fix would break. packages/system/kuberture/values.yaml
|
||||
# carries an `image:` map with a `tag:` and no `repository:` — shape 2 wants a
|
||||
# `digest` key and shape 3 a `repository` key, so shape 1's scrape of the bare
|
||||
# tag scalar is the ONLY rule that ever sees that image's digest. Discarding
|
||||
# host-less refs rather than comparing them on their digest would silently stop
|
||||
# proving it unchanged, which is a hole in the same guard being repaired here.
|
||||
# Asserted from the rejecting direction, because that is the direction able to
|
||||
# go red: the digest moves and nothing else does, so the digest-set comparison
|
||||
# is the only leg that can fire — artifact and release tree stay byte-identical,
|
||||
# no rc string appears in the candidate, and the installer values match.
|
||||
_write_repository_less_map() {
|
||||
cat > "$1" <<EOF
|
||||
kuberture:
|
||||
image:
|
||||
tag: 0.1.1@sha256:$2
|
||||
EOF
|
||||
}
|
||||
|
||||
_add_repository_less_package() {
|
||||
t="$1"
|
||||
mkdir -p "$t/release/system/kuberture" "$t/artifact/system/kuberture" \
|
||||
"$t/rc-artifact/system/kuberture"
|
||||
_repoless_stable=1111111111111111111111111111111111111111111111111111111111111111
|
||||
_repoless_rc=2222222222222222222222222222222222222222222222222222222222222222
|
||||
_write_repository_less_map "$t/release/system/kuberture/values.yaml" "$_repoless_stable"
|
||||
_write_repository_less_map "$t/artifact/system/kuberture/values.yaml" "$_repoless_stable"
|
||||
_write_repository_less_map "$t/rc-artifact/system/kuberture/values.yaml" "$_repoless_rc"
|
||||
}
|
||||
|
||||
@test "rejects a changed digest carried only by a repository-less image map" {
|
||||
tmp="$(_test_workspace)/fixture"
|
||||
_make_verify_fixture "$tmp"
|
||||
_add_repository_less_package "$tmp"
|
||||
|
||||
rc=0
|
||||
MOCK_ARTIFACT="$tmp/artifact" MOCK_RC_ARTIFACT="$tmp/rc-artifact" MOCK_FLUX_LOG="$tmp/flux.log" \
|
||||
VERIFY_PACKAGES_WORKDIR="$tmp/work" \
|
||||
PATH="$tmp/bin:$PATH" \
|
||||
hack/verify-promoted-packages.sh 9.9.9 "$tmp/release" \
|
||||
> "$tmp/out" 2> "$tmp/err" || rc=$?
|
||||
|
||||
[ "$rc" -ne 0 ]
|
||||
grep -q 'promotion changed the container repository/digest set' "$tmp/err"
|
||||
# The rejection has to come from the digest set and not from a neighbouring leg
|
||||
# rejecting a tree this test never perturbed, or it would still pass with the
|
||||
# digest dropped from the comparison entirely. Counted rather than negated with
|
||||
# `!`, which suppresses errexit and would pass regardless.
|
||||
for _other in 'contain different files' 'differs from release tree' \
|
||||
'still carries rc image references' 'installer values differ'; do
|
||||
count="$(grep -c "$_other" "$tmp/err" || true)"
|
||||
[ "${count:-0}" -eq 0 ]
|
||||
done
|
||||
}
|
||||
Reference in new issue
Block a user