test(kube-ovn): compare image copies against upstream's Dockerfile

packages/system/kubeovn/images/kubeovn/Dockerfile is a hand-kept copy
of upstream's dist/images/Dockerfile, and `make update` rewrites only
its ARG VERSION and ARG TAG. A binary upstream starts shipping
therefore does not reach our image on its own, and nothing read the two
files together: kube-ovn-bfdd-supervisor went missing that way with the
whole suite green.

hack/check-kubeovn-image-copies.sh normalizes the COPY instructions of
both files and reports either side's surplus. It compares only the
stages built FROM kubeovn/kube-ovn-base, because our builder stage
clones and patches kube-ovn and its COPY lines have no upstream
counterpart, and it treats an empty result as a broken check rather
than agreement.

`make update` now runs it against the tag it pulls, so the next bump
stops on a mismatch, and hack/check-kubeovn-image-copies.bats runs it
on every CI run against a verbatim copy of upstream's Dockerfile at the
vendored appVersion.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
This commit is contained in:
IvanHunters committed 2026-09-22 17:10:05 +03:00
1 parent b59510abe6
commit a4547a6fe0
3 files changed
+280

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/check-kubeovn-image-copies.sh.
#
# packages/system/kubeovn/images/kubeovn/Dockerfile is a hand-kept copy of
# upstream's dist/images/Dockerfile, and `make update` rewrites only ARG VERSION
# and ARG TAG in it. When v1.15.26 added kube-ovn-bfdd-supervisor to the image,
# our copy kept the v1.15.10 set and the binary went missing, which nothing in
# the tree noticed. The checker compares what each file copies, and this suite
# runs it over the real Dockerfile against a verbatim copy of upstream's for the
# vendored appVersion -- a synthetic pair alone would only assert the shape we
# already imagined.
#
# cozytest.sh's awk parser recognizes only @test blocks and a bare `}` on its
# own line; there is no bats `run` or `$status`. Assertions are direct shell
# tests, and an expected failure is written as `if CHECK ...; then ... exit 1`.
# -----------------------------------------------------------------------------
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
CHECK="$REPO_ROOT/hack/check-kubeovn-image-copies.sh"
OURS="$REPO_ROOT/packages/system/kubeovn/images/kubeovn/Dockerfile"
CHART="$REPO_ROOT/packages/system/kubeovn/charts/kube-ovn/Chart.yaml"
# Upstream dist/images/Dockerfile at v1.15.26, copied verbatim. Bump it in the
# same change that bumps the chart: `make update` re-runs the same comparison
# against the tag it pulls, so the two cannot disagree for long.
UPSTREAM_VERSION=1.15.26
upstream_dockerfile() {
cat <<'EOF'
# syntax=docker/dockerfile:1
ARG VERSION
ARG BASE_TAG=$VERSION
FROM kubeovn/kube-ovn-base:$BASE_TAG AS setcap
COPY *.sh /kube-ovn/
COPY kubectl-ko /kube-ovn/kubectl-ko
COPY 01-kube-ovn.conflist /kube-ovn/01-kube-ovn.conflist
COPY kube-ovn /kube-ovn/kube-ovn
COPY kube-ovn-cmd /kube-ovn/kube-ovn-cmd
COPY kube-ovn-bfdd-supervisor /kube-ovn/kube-ovn-bfdd-supervisor
COPY kube-ovn-daemon /kube-ovn/kube-ovn-daemon
COPY kube-ovn-controller /kube-ovn/kube-ovn-controller
RUN ln -s /kube-ovn/kube-ovn-cmd /kube-ovn/kube-ovn-monitor && \
ln -s /kube-ovn/kube-ovn-cmd /kube-ovn/kube-ovn-speaker && \
ln -s /kube-ovn/kube-ovn-cmd /kube-ovn/kube-ovn-webhook && \
ln -s /kube-ovn/kube-ovn-cmd /kube-ovn/kube-ovn-leader-checker && \
ln -s /kube-ovn/kube-ovn-cmd /kube-ovn/kube-ovn-ic-controller && \
ln -s /kube-ovn/kube-ovn-controller /kube-ovn/kube-ovn-pinger && \
setcap CAP_NET_BIND_SERVICE+eip /kube-ovn/kube-ovn-cmd && \
setcap CAP_NET_RAW,CAP_NET_BIND_SERVICE+eip /kube-ovn/kube-ovn-controller && \
setcap CAP_NET_ADMIN,CAP_NET_RAW,CAP_NET_BIND_SERVICE,CAP_SYS_ADMIN+eip /kube-ovn/kube-ovn-daemon
FROM kubeovn/kube-ovn-base:$BASE_TAG
COPY --chmod=0644 logrotate/* /etc/logrotate.d/
COPY grace_stop_ovn_controller /usr/share/ovn/scripts/grace_stop_ovn_controller
COPY --from=setcap /kube-ovn /kube-ovn
RUN /kube-ovn/iptables-wrapper-installer.sh --no-sanity-check
WORKDIR /kube-ovn
EOF
}
@test "the fixture tracks the vendored chart version" {
# Chart.yaml quotes the value; the package Makefile leans on the shell to
# strip those, this runs the awk output through a comparison instead.
appversion=$(awk '$1 == "appVersion:" {gsub(/"/, "", $2); print $2}' "$CHART")
if [ "$appversion" != "$UPSTREAM_VERSION" ]; then
echo "FAIL: chart appVersion is $appversion, fixture is v$UPSTREAM_VERSION" >&2
echo "Refresh upstream_dockerfile() from dist/images/Dockerfile at v$appversion." >&2
return 1
fi
}
@test "our Dockerfile copies everything upstream's does" {
upstream_dockerfile | "$CHECK" "$OURS" -
}
@test "a binary upstream added and we did not is reported" {
ours=$(mktemp)
upstream=$(mktemp)
upstream_dockerfile > "$upstream"
grep -v 'kube-ovn-bfdd-supervisor' "$OURS" > "$ours"
if "$CHECK" "$ours" "$upstream" 2>/dev/null; then
echo "FAIL: a missing binary passed the check" >&2
return 1
fi
if ! "$CHECK" "$ours" "$upstream" 2>&1 | grep -q 'kube-ovn-bfdd-supervisor'; then
echo "FAIL: the report does not name the missing binary" >&2
return 1
fi
}
@test "a copy we make and upstream does not is reported" {
ours=$(mktemp)
upstream=$(mktemp)
upstream_dockerfile > "$upstream"
sed -e 's|^COPY --from=builder /source/dist/images/kube-ovn-cmd .*|&\nCOPY --from=builder /source/dist/images/kube-ovn-invented /kube-ovn/kube-ovn-invented|' "$OURS" > "$ours"
if "$CHECK" "$ours" "$upstream" 2>/dev/null; then
echo "FAIL: an extra copy passed the check" >&2
return 1
fi
if ! "$CHECK" "$ours" "$upstream" 2>&1 | grep -q 'kube-ovn-invented'; then
echo "FAIL: the report does not name the extra copy" >&2
return 1
fi
}
@test "COPY lines in the builder stage are ours alone and not compared" {
# `COPY patches /patches` and the regression test have no upstream
# counterpart: they feed our clone-and-patch build, which upstream does not
# have. Reading them would make the check fail on every run.
ours=$(mktemp)
upstream=$(mktemp)
upstream_dockerfile > "$upstream"
sed -e 's|^COPY patches /patches$|COPY patches /patches\nCOPY extra-build-input /some/where|' "$OURS" > "$ours"
"$CHECK" "$ours" "$upstream"
}
@test "a destination that differs is reported even when the source matches" {
ours=$(mktemp)
upstream=$(mktemp)
upstream_dockerfile > "$upstream"
sed -e 's|/kube-ovn/kube-ovn-daemon$|/kube-ovn/kube-ovn-daemon-typo|' "$OURS" > "$ours"
if "$CHECK" "$ours" "$upstream" 2>/dev/null; then
echo "FAIL: a retargeted copy passed the check" >&2
return 1
fi
}
@test "the checker reads the file it is given and not the tree" {
empty=$(mktemp)
upstream=$(mktemp)
upstream_dockerfile > "$upstream"
printf 'FROM scratch\n' > "$empty"
if "$CHECK" "$empty" "$upstream" 2>/dev/null; then
echo "FAIL: a Dockerfile with no copies at all passed the check" >&2
return 1
fi
}
+123
View File
@@ -0,0 +1,123 @@
#!/bin/sh
# Compare what packages/system/kubeovn/images/kubeovn/Dockerfile copies into the
# kube-ovn image against upstream's dist/images/Dockerfile, which it is a
# hand-kept copy of.
#
# `make update` in that package rewrites only ARG VERSION and ARG TAG, so a
# binary upstream starts shipping does not reach our image on its own: v1.15.26
# added kube-ovn-bfdd-supervisor, our copy kept the v1.15.10 set, and a
# VpcEgressGateway with spec.bfd got a container whose command did not exist.
#
# Usage: hack/check-kubeovn-image-copies.sh OURS UPSTREAM
# Either path may be `-` to read that side from stdin.
set -eu
[ $# -eq 2 ] || {
echo "Usage: $0 OURS UPSTREAM (either path may be '-' for stdin)" >&2
exit 2
}
OURS=$1
UPSTREAM=$2
# Normalize the COPY instructions of every stage built FROM kubeovn/kube-ovn-base
# to `<source>... <destination>` lines.
#
# Only those stages are comparable. Our builder stage clones and patches
# kube-ovn, which upstream does not do at all, so its COPY lines have no
# counterpart and reading them would make every run fail.
#
# Flags are dropped so that `--from=builder /source/dist/images/kube-ovn` and a
# bare `kube-ovn` read the same: upstream copies out of a build context that our
# multi-stage build reproduces under /source/dist/images.
copies() {
if [ "$1" = "-" ]; then
cat
else
cat -- "$1"
fi | awk '
function strip(path) {
sub(/^\/source\/dist\/images\//, "", path)
return path
}
# Fold a continued instruction into one logical line before reading it.
{
line = line $0
if (line ~ /\\$/) {
sub(/\\$/, " ", line)
next
}
}
line ~ /^[[:space:]]*FROM[[:space:]]/ {
in_scope = (line ~ /kubeovn\/kube-ovn-base/)
line = ""
next
}
in_scope && line ~ /^[[:space:]]*COPY[[:space:]]/ {
$0 = line
out = ""
for (i = 2; i <= NF; i++) {
if ($i ~ /^--/) continue
out = out (out == "" ? "" : " ") strip($i)
}
if (out != "") print out
}
{ line = "" }
' | LC_ALL=C sort
}
# Lines on stdin that do not appear in the newline-separated set $1.
#
# The set is fed through the same stream rather than `awk -v`: a multi-line -v
# value is a GNU extension, and the awk on a maintainer's macOS rejects it.
lines_not_in() {
{
printf '%s\n' "$1" | sed 's/^/S /'
sed 's/^/C /'
} | awk '
$1 == "S" { sub(/^S /, ""); seen[$0] = 1; next }
{ sub(/^C /, ""); if ($0 != "" && !($0 in seen)) print }
'
}
ours_copies=$(copies "$OURS")
upstream_copies=$(copies "$UPSTREAM")
# A side with no copies at all means the stage markers stopped matching, not
# that the two agree. Reporting that as a pass is how this check would quietly
# stop working the next time the base image is renamed.
if [ -z "$ours_copies" ]; then
echo "ERROR: no COPY instructions found in a kubeovn/kube-ovn-base stage of $OURS" >&2
exit 1
fi
if [ -z "$upstream_copies" ]; then
echo "ERROR: no COPY instructions found in a kubeovn/kube-ovn-base stage of $UPSTREAM" >&2
exit 1
fi
missing=$(printf '%s\n' "$upstream_copies" | lines_not_in "$ours_copies")
extra=$(printf '%s\n' "$ours_copies" | lines_not_in "$upstream_copies")
status=0
if [ -n "$missing" ]; then
echo "ERROR: upstream copies these into the image and we do not:" >&2
printf '%s\n' "$missing" | sed 's/^/ COPY /' >&2
status=1
fi
if [ -n "$extra" ]; then
echo "ERROR: we copy these into the image and upstream does not:" >&2
printf '%s\n' "$extra" | sed 's/^/ COPY /' >&2
status=1
fi
if [ "$status" -ne 0 ]; then
echo >&2
echo "packages/system/kubeovn/images/kubeovn/Dockerfile is a copy of upstream's" >&2
echo "dist/images/Dockerfile. Bring the two back in step, keeping our" >&2
echo "--from=builder /source/dist/images/ prefix." >&2
fi
exit "$status"
+10
View File
@@ -20,6 +20,16 @@ update:
version=$$(awk '$$1 == "appVersion:" {print $$2}' charts/kube-ovn/Chart.yaml) && \
$(SED_INPLACE) -e "s/ARG VERSION=.*/ARG VERSION=$${version}/" images/kubeovn/Dockerfile && \
$(SED_INPLACE) -e "s/ARG TAG=.*/ARG TAG=v$${version}/" images/kubeovn/Dockerfile
# images/kubeovn/Dockerfile is a copy of upstream's dist/images/Dockerfile
# and only its ARG lines are rewritten above, so a binary upstream starts
# shipping does not reach our image on its own. v1.15.26 added
# kube-ovn-bfdd-supervisor that way and nothing noticed until the container
# it backs could not exec. hack/check-kubeovn-image-copies.bats makes the
# same comparison on every CI run, against a fixture this step tells you to
# refresh.
version=$$(awk '$$1 == "appVersion:" {gsub(/"/, "", $$2); print $$2}' charts/kube-ovn/Chart.yaml) && \
curl -sSfL https://raw.githubusercontent.com/kubeovn/kube-ovn/v$${version}/dist/images/Dockerfile | \
../../../hack/check-kubeovn-image-copies.sh images/kubeovn/Dockerfile -
image:
docker buildx build images/kubeovn \