mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 09:13:01 +02:00
test: verify container upgrade
This commit adds four (small) container images to the Infamy test container which are used in the new container upgrade test. The test verifies that a mutable container can be upgraded and that old images are properly cleaned up from the container store. Fixes #624 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# Current container image
|
||||
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.5
|
||||
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.6
|
||||
|
||||
ixdir=$(readlink -f "$testdir/..")
|
||||
logdir=$(readlink -f "$testdir/.log")
|
||||
|
||||
@@ -10,6 +10,7 @@ Tests verifying Infix Docker container support:
|
||||
- Connecting containers with VETH pairs to standard Linux bridges
|
||||
- Assigning physical Ethernet interfaces to containers
|
||||
- Container upgrades with persistent volume data
|
||||
- Container upgrade using RPC with cleanup of old image
|
||||
- Firewall container running in host network mode with full privileges
|
||||
|
||||
include::container_basic/Readme.adoc[]
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
- name: Container Volume Persistence
|
||||
case: container_volume/test.py
|
||||
|
||||
- name: Container Upgrade
|
||||
case: upgrade/test.py
|
||||
|
||||
- name: Basic Firewall Container
|
||||
case: container_firewall_basic/test.py
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
# Download curios-httpd container images for upgrade testing
|
||||
# This script is called during Docker image build to pre-populate
|
||||
# the test container with the necessary OCI archives.
|
||||
|
||||
set -e
|
||||
|
||||
DEST_DIR="${1:-/srv}"
|
||||
IMAGE_BASE="ghcr.io/kernelkit/curios-httpd"
|
||||
VERSIONS="24.05.0 24.11.0"
|
||||
ARCHS="linux/amd64 linux/arm64"
|
||||
|
||||
echo "Downloading curios-httpd images to $DEST_DIR..."
|
||||
mkdir -p "$DEST_DIR"
|
||||
|
||||
for ver in $VERSIONS; do
|
||||
for arch in $ARCHS; do
|
||||
# Create architecture-specific filename
|
||||
arch_suffix=$(echo "$arch" | sed 's|linux/||')
|
||||
output="$DEST_DIR/curios-httpd-${ver}-${arch_suffix}.tar"
|
||||
|
||||
echo "Fetching ${IMAGE_BASE}:${ver} for ${arch}..."
|
||||
skopeo copy --override-arch "${arch#linux/}" \
|
||||
"docker://${IMAGE_BASE}:${ver}" \
|
||||
"oci-archive:${output}"
|
||||
|
||||
# Check if already gzipped and compress if needed
|
||||
output_gz="${output}.gz"
|
||||
if file "$output" | grep -q "gzip compressed"; then
|
||||
echo "File ${output} is already gzipped, renaming..."
|
||||
mv "$output" "$output_gz"
|
||||
else
|
||||
echo "Compressing ${output}..."
|
||||
gzip "${output}"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "Download complete!"
|
||||
ls -lh "$DEST_DIR"
|
||||
@@ -0,0 +1,34 @@
|
||||
=== Container Upgrade
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_containers/upgrade]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify container upgrade functionality by testing the optimization
|
||||
that skips recreation when the container configuration hasn't changed.
|
||||
|
||||
This test uses two versions of curios-httpd (24.05.0 and 24.11.0) served
|
||||
over HTTP with :latest tag. The test verifies that:
|
||||
1. Container starts successfully from the :latest image
|
||||
2. When the :latest tag points to a new version, upgrade is triggered
|
||||
3. The container runs the new version after upgrade
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[Container Upgrade topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to target DUT
|
||||
. Detect target architecture
|
||||
. Set up isolated network and file server
|
||||
. Create symlink for curios-httpd:latest -> 24.05.0
|
||||
. Create container 'web' from curios-httpd:latest (24.05.0)
|
||||
. Verify container 'web' has started with version 24.05.0
|
||||
. Update symlink to point to curios-httpd:24.11.0
|
||||
. Trigger container upgrade by calling upgrade action
|
||||
. Wait for container 'web' to complete uprgade
|
||||
. Verify container 'web' is running new version 24.11.0
|
||||
. Verify old image was pruned and disk usage is reasonable
|
||||
|
||||
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Container Upgrade
|
||||
|
||||
Verify container upgrade functionality by testing the optimization
|
||||
that skips recreation when the container configuration hasn't changed.
|
||||
|
||||
This test uses two versions of curios-httpd (24.05.0 and 24.11.0) served
|
||||
over HTTP with :latest tag. The test verifies that:
|
||||
1. Container starts successfully from the :latest image
|
||||
2. When the :latest tag points to a new version, upgrade is triggered
|
||||
3. The container runs the new version after upgrade
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import infamy
|
||||
import infamy.file_server as srv
|
||||
from infamy.util import until
|
||||
|
||||
SRVPORT = 8008
|
||||
SRVDIR = "/srv"
|
||||
|
||||
with infamy.Test() as test:
|
||||
NAME = "web"
|
||||
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
tgtssh = env.attach("target", "mgmt", "ssh")
|
||||
|
||||
if not target.has_model("infix-containers"):
|
||||
test.skip()
|
||||
|
||||
_, hport = env.ltop.xlate("host", "data")
|
||||
_, tport = env.ltop.xlate("target", "data")
|
||||
|
||||
with test.step("Detect target architecture"):
|
||||
# Query operational datastore for machine architecture
|
||||
system_state = target.get_data("/ietf-system:system-state")
|
||||
arch = system_state["system-state"]["platform"]["machine"]
|
||||
|
||||
# Map kernel arch to our image naming
|
||||
arch_map = {
|
||||
"x86_64": "amd64",
|
||||
"aarch64": "arm64",
|
||||
"armv7l": "arm64", # Fallback for ARM variants
|
||||
}
|
||||
image_arch = arch_map.get(arch, "amd64")
|
||||
print(f"Detected architecture: {arch} -> using {image_arch} images")
|
||||
|
||||
with test.step("Set up isolated network and file server"):
|
||||
netns = infamy.IsolatedMacVlan(hport).start()
|
||||
netns.addip("192.168.0.1")
|
||||
|
||||
target.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [
|
||||
{
|
||||
"name": tport,
|
||||
"ipv4": {
|
||||
"address": [
|
||||
{
|
||||
"ip": "192.168.0.2",
|
||||
"prefix-length": 24
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
netns.must_reach("192.168.0.2")
|
||||
|
||||
with srv.FileServer(netns, "192.168.0.1", SRVPORT, SRVDIR):
|
||||
with test.step("Create symlink for curios-httpd:latest -> 24.05.0"):
|
||||
# Create symlink in the file server directory
|
||||
old_img = f"{SRVDIR}/curios-httpd-24.05.0-{image_arch}.tar.gz"
|
||||
new_img = f"{SRVDIR}/curios-httpd-24.11.0-{image_arch}.tar.gz"
|
||||
latest_link = f"{SRVDIR}/curios-httpd-latest.tar.gz"
|
||||
|
||||
# Remove any existing symlink
|
||||
if os.path.exists(latest_link):
|
||||
os.unlink(latest_link)
|
||||
|
||||
# Point to old version
|
||||
os.symlink(old_img, latest_link)
|
||||
print(f"Created symlink: {latest_link} -> {old_img}")
|
||||
|
||||
with test.step("Create container 'web' from curios-httpd:latest (24.05.0)"):
|
||||
target.put_config_dict("infix-containers", {
|
||||
"containers": {
|
||||
"container": [
|
||||
{
|
||||
"name": f"{NAME}",
|
||||
"image": f"http://192.168.0.1:{SRVPORT}/curios-httpd-latest.tar.gz",
|
||||
"command": "/usr/sbin/httpd -f -v -p 91",
|
||||
"network": {
|
||||
"host": True
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Verify container 'web' has started with version 24.05.0"):
|
||||
c = infamy.Container(target)
|
||||
until(lambda: c.running(NAME), attempts=60)
|
||||
|
||||
# Get initial operational data to capture container ID and image ID
|
||||
containers_data = target.get_data("/infix-containers:containers")
|
||||
container = containers_data["containers"]["container"][NAME]
|
||||
initial_container_id = container["id"]
|
||||
initial_image_id = container["image-id"]
|
||||
|
||||
print(f"Container started with container-id: {initial_container_id[:12]}...")
|
||||
print(f" and image-id: {initial_image_id[:12]}...")
|
||||
|
||||
# Get baseline disk usage for /var/lib/containers
|
||||
result = tgtssh.runsh("doas du -s /var/lib/containers")
|
||||
initial_disk_usage = int(result.stdout.split()[0])
|
||||
print(f"Disk usage after initial creation: {initial_disk_usage} KiB")
|
||||
|
||||
with test.step("Update symlink to point to curios-httpd:24.11.0"):
|
||||
# Remove old symlink and point to new version
|
||||
os.unlink(latest_link)
|
||||
os.symlink(new_img, latest_link)
|
||||
print(f"Updated symlink: {latest_link} -> {new_img}")
|
||||
|
||||
with test.step("Trigger container upgrade by calling upgrade action"):
|
||||
c = infamy.Container(target)
|
||||
c.action(NAME, "upgrade")
|
||||
|
||||
with test.step("Wait for container 'web' to complete uprgade"):
|
||||
time.sleep(3)
|
||||
until(lambda: c.running(NAME), attempts=30)
|
||||
|
||||
with test.step("Verify container 'web' is running new version 24.11.0"):
|
||||
c = infamy.Container(target)
|
||||
# Wait for upgrade to complete and container to restart
|
||||
until(lambda: c.running(NAME), attempts=60)
|
||||
|
||||
# Get operational data after upgrade
|
||||
containers_data = target.get_data("/infix-containers:containers")
|
||||
container = containers_data["containers"]["container"][NAME]
|
||||
new_container_id = container["id"]
|
||||
new_image_id = container["image-id"]
|
||||
|
||||
print(f"After upgrade container-id: {new_container_id[:12]}...")
|
||||
print(f" image-id: {new_image_id[:12]}...")
|
||||
|
||||
# Verify that both IDs have changed
|
||||
if new_container_id == initial_container_id:
|
||||
test.fail("Container ID did not change after upgrade!")
|
||||
if new_image_id == initial_image_id:
|
||||
test.fail("Image ID did not change after upgrade!")
|
||||
|
||||
print("✓ Both container ID and image ID changed after upgrade")
|
||||
|
||||
with test.step("Verify old image was pruned and disk usage is reasonable"):
|
||||
# We expect minimal growth — the new image replaces old
|
||||
# image, and they are each around 500-600 KiB. We allow for
|
||||
# some overhead, but should be < 200 KiB. If the old image
|
||||
# is not pruned properly, we'll see ~600 KiB extra growth.
|
||||
MAX_ACCEPTABLE_GROWTH = 1000
|
||||
|
||||
# Get disk usage after upgrade
|
||||
result = tgtssh.runsh("doas du -s /var/lib/containers")
|
||||
final_disk_usage = int(result.stdout.split()[0])
|
||||
print(f"Disk usage after upgrade: {final_disk_usage} KiB")
|
||||
|
||||
# Calculate the difference
|
||||
disk_growth = final_disk_usage - initial_disk_usage
|
||||
print(f"Disk usage growth: {disk_growth} KiB")
|
||||
|
||||
if disk_growth > MAX_ACCEPTABLE_GROWTH:
|
||||
test.fail(f"Disk usage grew by {disk_growth} KiB (expected < {MAX_ACCEPTABLE_GROWTH} KiB). "
|
||||
f"Old image may not have been pruned!")
|
||||
|
||||
print(f"✓ Disk usage growth is acceptable ({disk_growth} KiB < {MAX_ACCEPTABLE_GROWTH} KiB)")
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,24 @@
|
||||
graph "1x2" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+80";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt> mgmt | <data> data }",
|
||||
pos="0,12!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
target [
|
||||
label="{ <mgmt> mgmt | <data> data } | target",
|
||||
pos="10,12!",
|
||||
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey]
|
||||
host:data -- target:data [color=black]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 1x2 Pages: 1 -->
|
||||
<svg width="424pt" height="55pt"
|
||||
viewBox="0.00 0.00 424.03 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
|
||||
<title>1x2</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 420.03,-51 420.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
|
||||
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
|
||||
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-46.5 416.03,-46.5 416.03,-0.5 300.03,-0.5"/>
|
||||
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
|
||||
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-46.5 "/>
|
||||
<text text-anchor="middle" x="383.03" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M100,-35.5C100,-35.5 300.03,-35.5 300.03,-35.5"/>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data--target:data</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 300.03,-11.5 300.03,-11.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
+11
-5
@@ -8,6 +8,7 @@ RUN apk add --no-cache \
|
||||
e2tools \
|
||||
ethtool \
|
||||
fakeroot \
|
||||
file \
|
||||
gcc \
|
||||
git \
|
||||
graphviz \
|
||||
@@ -25,6 +26,7 @@ RUN apk add --no-cache \
|
||||
qemu-img \
|
||||
qemu-system-x86_64 \
|
||||
ruby-mustache \
|
||||
skopeo \
|
||||
socat \
|
||||
squashfs-tools \
|
||||
sshpass \
|
||||
@@ -38,20 +40,24 @@ RUN cd /tmp/mtools-$MTOOL_VERSION && make && make install
|
||||
|
||||
# Alpine's QEMU package does not bundle this for some reason, copied
|
||||
# from Ubuntu
|
||||
COPY qemu-ifup /etc
|
||||
COPY docker/qemu-ifup /etc
|
||||
|
||||
# Needed to let qeneth find mustache(1)
|
||||
ENV PATH="${PATH}:/usr/lib/ruby/gems/3.2.0/bin"
|
||||
|
||||
# Install all python packages used by the tests
|
||||
COPY init-venv.sh /root
|
||||
COPY pip-requirements.txt /root
|
||||
COPY docker/init-venv.sh /root
|
||||
COPY docker/pip-requirements.txt /root
|
||||
|
||||
# Add bootstrap YANG models, the rest will be downloaded from the device
|
||||
ADD yang /root/yang
|
||||
ADD docker/yang /root/yang
|
||||
|
||||
RUN ~/init-venv.sh ~/pip-requirements.txt
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
# Download container images for upgrade testing
|
||||
COPY case/infix_containers/upgrade/download.sh /tmp/download.sh
|
||||
RUN /tmp/download.sh /srv && rm /tmp/download.sh
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["/bin/sh"]
|
||||
|
||||
@@ -16,7 +16,8 @@ the image, e.g., with missing Alpine packages.
|
||||
here: <https://github.com/kernelkit/infix/pkgs/container/infix-test>
|
||||
in this example we use version 0.4:
|
||||
|
||||
docker build -t ghcr.io/kernelkit/infix-test:0.4 .
|
||||
cd test/
|
||||
docker build -f docker/Dockerfile -t ghcr.io/kernelkit/infix-test:0.4 .
|
||||
|
||||
3. Update the `test/.env` file to use the new version
|
||||
4. Verify your new image works properly (remember to remove your `~/.infix/venv`)
|
||||
|
||||
Reference in New Issue
Block a user