From 6cfbb5d98e4db5e14b52f79269f088cf3b650bdb Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Thu, 16 Jul 2026 13:41:38 -0700 Subject: [PATCH 01/11] fix: add strict deb/rpm version match in VHD content test The VHD content test previously only validated major.minor.patch for containerd and crictl (via binary --version), ignoring the hotfix suffix (e.g. u1 vs u2). This allowed mismatches between components.json and the actual installed package to go undetected until e2e. Changes: - Add assertPackageVersion helper that uses dpkg-query (deb) or rpm (RPM-based) to compare the full installed version against components.json - Resolve the real package name dynamically from components.json renovateTag via getPackageJSON (handles moby-containerd on Ubuntu, containerd2 on Azure Linux 3.0, etc.) - Fail fast when package name cannot be resolved from renovateTag - Fix err() call signatures to match the two-argument format - Add local test script for package name extraction verification --- .../packer/test/linux-vhd-content-test.sh | 95 +++++++++++++----- .../packer/test/test_pkg_name_extraction.sh | 97 +++++++++++++++++++ 2 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 vhdbuilder/packer/test/test_pkg_name_extraction.sh diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index 493ad55b0cf..34da17b0856 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -277,11 +277,17 @@ testPackagesInstalled() { # For example, in Ubuntu, we use apt-get to install packages. # We can simply execute the command to verify the package version. case "$name" in - "kubernetes-cri-tools") - testCriCtl "$version" - ;; - "containerd") - testContainerd "$version" + "kubernetes-cri-tools"|"containerd") + # Resolve the actual installed package name from components.json renovateTag. + # The components.json "name" field may differ from the real package name + # (e.g. "containerd" -> moby-containerd on Ubuntu, containerd2 on Azure Linux 3.0). + local pkgName + pkgName=$(getPackageJSON "$p" "${OS}" "${OS_VERSION}" "${OS_VARIANT}" | jq -r '.versionsV2[0].renovateTag // empty' | sed -n 's/.*name=\([^,]*\).*/\1/p' | xargs) + if [ "$name" = "kubernetes-cri-tools" ]; then + testCriCtl "$version" "${pkgName:-}" + else + testContainerd "$version" "${pkgName:-}" + fi ;; esac break @@ -1964,26 +1970,63 @@ testAKSNodeControllerService() { echo "$test:Finish" } +# assertPackageVersion verifies that the installed deb/rpm package version matches +# the expected full version string from components.json (including hotfix suffix). +# This catches drift between what the package manager installs and what components.json +# specifies at VHD build time rather than in e2e. +assertPackageVersion() { + local test="$1" + local packageName="$2" + local expectedVersion="$3" + + local installedVersion="" + if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' "$packageName" 2>/dev/null | grep -q "install ok installed"; then + # dpkg versions may include an epoch prefix (e.g. "1:..."); strip it for comparison with components.json. + installedVersion=$(dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null | sed 's/^[0-9]*://') + elif command -v rpm >/dev/null 2>&1 && rpm -q "$packageName" >/dev/null 2>&1; then + installedVersion=$(rpm -q --queryformat '%{VERSION}-%{RELEASE}' "$packageName" 2>/dev/null) + else + err "$test" "$packageName is not installed" + return 1 + fi + + echo "$test: checking if installed $packageName version '$installedVersion' matches expected '$expectedVersion'" + if [ "$installedVersion" != "$expectedVersion" ]; then + err "$test" "installed $packageName version '$installedVersion' does not match expected '$expectedVersion' from components.json" + return 1 + fi + return 0 +} + testCriCtl() { expectedVersion="${1}" + local installedPackageName="${2}" local test="testCriCtl" echo "$test: Start" # If the version defined in components.json is , that means it will use whatever version is installed on the system. # Therefore, we will just skip the test. if [ "$expectedVersion" = "" ]; then - echo "$test: Skipping test for containerd version, as expected version is " + echo "$test: Skipping test for crictl version, as expected version is " return 0 fi - # the expectedVersion looks like this, "1.32.0-ubuntu24.04u3", need to extract the version number. - expectedVersion=$(echo $expectedVersion | cut -d'-' -f1) - # use command `crictl --version` to get the version - local crictl_version=$(crictl --version) + # Strict match: verify the full deb/rpm package version matches components.json + if [ -z "$installedPackageName" ]; then + err "$test" "could not resolve package name from components.json renovateTag" + return 1 + fi + assertPackageVersion "$test" "$installedPackageName" "$expectedVersion" || return 1 + + # Verify the binary reports the expected major.minor.patch version. + local expectedMajorMinorPatch + expectedMajorMinorPatch=$(echo "$expectedVersion" | cut -d'-' -f1) + local crictl_version + crictl_version=$(crictl --version) # the output of crictl_version looks like this "crictl version 1.32.0", need to extract the version number. crictl_version=$(echo $crictl_version | cut -d' ' -f3) - echo "$test: checking if crictl version is $expectedVersion" - if [ "$crictl_version" != "$expectedVersion" ]; then - err "$test: crictl version is not $expectedVersion, instead it is $crictl_version" + echo "$test: checking if crictl binary version is $expectedMajorMinorPatch" + if [ "$crictl_version" != "$expectedMajorMinorPatch" ]; then + err "$test" "crictl binary version is not $expectedMajorMinorPatch, instead it is $crictl_version" return 1 fi echo "$test: Test finished successfully." @@ -1992,6 +2035,7 @@ testCriCtl() { testContainerd() { expectedVersion="${1}" + local installedPackageName="${2}" local test="testContainerd" echo "$test: Start" # If the version defined in components.json is , that means it will use whatever version is installed on the system. @@ -2000,19 +2044,26 @@ testContainerd() { echo "$test: Skipping test for containerd version, as expected version is " return 0 fi - # the expectedVersion looks like this, "1.6.24-0ubuntu1~24.04.1" or "2.0.0-6.azl3", we need to extract the major.minor.patch version only. - expectedVersion=$(echo $expectedVersion | cut -d'-' -f1) - # use command `containerd --version` to get the version - local containerd_version=$(containerd --version) - # the output of containerd_version looks like the followings. We need to extract the major.minor.patch version only. + + # Strict match: verify the full deb/rpm package version matches components.json + if [ -z "$installedPackageName" ]; then + err "$test" "could not resolve package name from components.json renovateTag" + return 1 + fi + assertPackageVersion "$test" "$installedPackageName" "$expectedVersion" || return 1 + + # Verify the containerd binary reports the expected major.minor.patch version. + local expectedMajorMinorPatch + expectedMajorMinorPatch=$(echo "$expectedVersion" | cut -d'-' -f1) + local containerd_version + containerd_version=$(containerd --version) # For containerd (v1): containerd github.com/containerd/containerd 1.6.26 # For containerd (v2): containerd github.com/containerd/containerd/v2 2.0.0 containerd_version=$(echo $containerd_version | cut -d' ' -f3) - # The version could be in the format "1.6.24-11-ubuntu1~24.04.1" or "2.0.0-6.azl3" or just "2.0.0", we need to extract the major.minor.patch version only. containerd_version=$(echo "$containerd_version" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') - echo "$test: checking if containerd version is $expectedVersion" - if [ "$containerd_version" != "$expectedVersion" ]; then - err "$test: containerd version is not $expectedVersion, instead it is $containerd_version" + echo "$test: checking if containerd binary version is $expectedMajorMinorPatch" + if [ "$containerd_version" != "$expectedMajorMinorPatch" ]; then + err "$test" "containerd binary version is not $expectedMajorMinorPatch, instead it is $containerd_version" return 1 fi echo "$test: Test finished successfully." diff --git a/vhdbuilder/packer/test/test_pkg_name_extraction.sh b/vhdbuilder/packer/test/test_pkg_name_extraction.sh new file mode 100644 index 00000000000..8d758aea389 --- /dev/null +++ b/vhdbuilder/packer/test/test_pkg_name_extraction.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Test that getPackageJSON + renovateTag extraction resolves correct package names +# for containerd and kubernetes-cri-tools across all supported OS variants. +# +# This is a local-only test script (not run in CI). ShellSpec integration was +# deferred because cse_helpers.sh has system dependencies that are hard to +# satisfy in the shellspec Docker environment. +# +# Usage: bash vhdbuilder/packer/test/test_pkg_name_extraction.sh + +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# Source cse_helpers.sh for getPackageJSON +# Set defaults for variables that cse_helpers.sh expects +OS_VARIANT="${OS_VARIANT:-DEFAULT}" +source "$REPO_ROOT/parts/linux/cloud-init/artifacts/cse_helpers.sh" 2>/dev/null +if ! type getPackageJSON >/dev/null 2>&1; then + echo "Failed to source cse_helpers.sh: getPackageJSON is not defined" >&2 + exit 1 +fi + +COMPONENTS_FILEPATH="$REPO_ROOT/parts/common/components.json" + +# Helper: extract package name from renovateTag via getPackageJSON +# Uses sed instead of grep -P for macOS compatibility +extractPkgName() { + local packageJson="$1" + local os="$2" + local osVersion="$3" + local osVariant="${4:-DEFAULT}" + getPackageJSON "$packageJson" "$os" "$osVersion" "$osVariant" \ + | jq -r '.versionsV2[0].renovateTag // empty' \ + | sed -n 's/.*name=\([^,]*\).*/\1/p' \ + | xargs +} + +PASS=0 +FAIL=0 + +assertPkgName() { + local description="$1" + local expected="$2" + local actual="$3" + + if [ "$actual" = "$expected" ]; then + echo "✓ $description: got '$actual'" + PASS=$((PASS + 1)) + else + echo "✗ $description: expected '$expected', got '$actual'" + FAIL=$((FAIL + 1)) + fi +} + +# Load containerd package JSON +containerdPkg=$(jq -c '.Packages[] | select(.name == "containerd")' "$COMPONENTS_FILEPATH") +criToolsPkg=$(jq -c '.Packages[] | select(.name == "kubernetes-cri-tools")' "$COMPONENTS_FILEPATH") + +echo "=== containerd package name extraction ===" + +# Ubuntu variants +result=$(extractPkgName "$containerdPkg" "UBUNTU" "24.04") +assertPkgName "containerd on Ubuntu 24.04" "moby-containerd" "$result" + +result=$(extractPkgName "$containerdPkg" "UBUNTU" "22.04") +assertPkgName "containerd on Ubuntu 22.04" "moby-containerd" "$result" + +result=$(extractPkgName "$containerdPkg" "UBUNTU" "20.04") +assertPkgName "containerd on Ubuntu 20.04" "moby-containerd" "$result" + +# Azure Linux +result=$(extractPkgName "$containerdPkg" "AZURELINUX" "3.0") +assertPkgName "containerd on Azure Linux 3.0" "containerd2" "$result" + +# Mariner +result=$(extractPkgName "$containerdPkg" "MARINER" "2.0" "DEFAULT") +assertPkgName "containerd on Mariner 2.0" "moby-containerd" "$result" + +echo "" +echo "=== kubernetes-cri-tools package name extraction ===" + +result=$(extractPkgName "$criToolsPkg" "UBUNTU" "24.04") +assertPkgName "cri-tools on Ubuntu 24.04" "kubernetes-cri-tools" "$result" + +result=$(extractPkgName "$criToolsPkg" "UBUNTU" "22.04") +assertPkgName "cri-tools on Ubuntu 22.04" "kubernetes-cri-tools" "$result" + +result=$(extractPkgName "$criToolsPkg" "AZURELINUX" "3.0") +assertPkgName "cri-tools on Azure Linux 3.0" "kubernetes-cri-tools" "$result" + +echo "" +echo "=== Results: $PASS passed, $FAIL failed ===" +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi From 6c4c25742f2af1d7d28a99ab48ac77d1705ec1e6 Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Thu, 16 Jul 2026 13:41:52 -0700 Subject: [PATCH 02/11] test: intentionally break containerd version to verify content test catches mismatch DO NOT MERGE - this commit will be reverted after CI confirms the content test fails at VHD build time (not at e2e). --- parts/common/components.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parts/common/components.json b/parts/common/components.json index 0ef54382592..37e73e6d70d 100644 --- a/parts/common/components.json +++ b/parts/common/components.json @@ -1069,7 +1069,7 @@ "versionsV2": [ { "renovateTag": "name=moby-containerd, repository=production, os=ubuntu, release=24.04", - "latestVersion": "2.3.2-ubuntu24.04u2" + "latestVersion": "2.3.2-ubuntu24.04u99" } ] }, From b8daf572dab77843b262aba45da9d974ee81dc6c Mon Sep 17 00:00:00 2001 From: Abigail Liang Date: Thu, 16 Jul 2026 13:48:53 -0700 Subject: [PATCH 03/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- vhdbuilder/packer/test/test_pkg_name_extraction.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vhdbuilder/packer/test/test_pkg_name_extraction.sh b/vhdbuilder/packer/test/test_pkg_name_extraction.sh index 8d758aea389..8dbb4eb1d99 100644 --- a/vhdbuilder/packer/test/test_pkg_name_extraction.sh +++ b/vhdbuilder/packer/test/test_pkg_name_extraction.sh @@ -16,8 +16,7 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # Source cse_helpers.sh for getPackageJSON # Set defaults for variables that cse_helpers.sh expects OS_VARIANT="${OS_VARIANT:-DEFAULT}" -source "$REPO_ROOT/parts/linux/cloud-init/artifacts/cse_helpers.sh" 2>/dev/null -if ! type getPackageJSON >/dev/null 2>&1; then +if ! source "$REPO_ROOT/parts/linux/cloud-init/artifacts/cse_helpers.sh" 2>/dev/null || ! type getPackageJSON >/dev/null 2>&1; then echo "Failed to source cse_helpers.sh: getPackageJSON is not defined" >&2 exit 1 fi From a4338d8ff4fa47f4fb7f6c53b26789de8a47ba2d Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Thu, 16 Jul 2026 15:38:19 -0700 Subject: [PATCH 04/11] Revert "test: intentionally break containerd version to verify content test catches mismatch" This reverts commit 6c4c25742f2af1d7d28a99ab48ac77d1705ec1e6. --- parts/common/components.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parts/common/components.json b/parts/common/components.json index 37e73e6d70d..0ef54382592 100644 --- a/parts/common/components.json +++ b/parts/common/components.json @@ -1069,7 +1069,7 @@ "versionsV2": [ { "renovateTag": "name=moby-containerd, repository=production, os=ubuntu, release=24.04", - "latestVersion": "2.3.2-ubuntu24.04u99" + "latestVersion": "2.3.2-ubuntu24.04u2" } ] }, From e39698df4192754f16231d19c92003cdf59d0999 Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Thu, 16 Jul 2026 16:59:51 -0700 Subject: [PATCH 05/11] ci: retrigger CI checks From 7b302f90c3367b8d66349983bdf7e41717eb3d9d Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Mon, 20 Jul 2026 12:56:08 -0700 Subject: [PATCH 06/11] test: use explicit package name mapping instead of renovateTag parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review feedback, replace renovateTag parsing with hardcoded package name mapping. renovateTag is metadata for Renovate bot and its format is not guaranteed stable — parsing it makes the test fragile to unrelated changes. Also remove the shellspec test since the mapping is now trivially explicit and doesn't warrant a separate unit test. --- .../packer/test/linux-vhd-content-test.sh | 26 +++-- .../packer/test/test_pkg_name_extraction.sh | 96 ------------------- 2 files changed, 16 insertions(+), 106 deletions(-) delete mode 100644 vhdbuilder/packer/test/test_pkg_name_extraction.sh diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index 34da17b0856..1a033cc74e4 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -277,17 +277,23 @@ testPackagesInstalled() { # For example, in Ubuntu, we use apt-get to install packages. # We can simply execute the command to verify the package version. case "$name" in - "kubernetes-cri-tools"|"containerd") - # Resolve the actual installed package name from components.json renovateTag. - # The components.json "name" field may differ from the real package name - # (e.g. "containerd" -> moby-containerd on Ubuntu, containerd2 on Azure Linux 3.0). + "kubernetes-cri-tools") + testCriCtl "$version" "kubernetes-cri-tools" + ;; + "containerd") + # The deb/rpm package name for containerd varies by OS: + # Ubuntu / Mariner 2.0: moby-containerd + # Azure Linux 3.0+: containerd2 local pkgName - pkgName=$(getPackageJSON "$p" "${OS}" "${OS_VERSION}" "${OS_VARIANT}" | jq -r '.versionsV2[0].renovateTag // empty' | sed -n 's/.*name=\([^,]*\).*/\1/p' | xargs) - if [ "$name" = "kubernetes-cri-tools" ]; then - testCriCtl "$version" "${pkgName:-}" - else - testContainerd "$version" "${pkgName:-}" - fi + case "$OS" in + "$AZURELINUX_OS_NAME") + pkgName="containerd2" + ;; + *) + pkgName="moby-containerd" + ;; + esac + testContainerd "$version" "$pkgName" ;; esac break diff --git a/vhdbuilder/packer/test/test_pkg_name_extraction.sh b/vhdbuilder/packer/test/test_pkg_name_extraction.sh deleted file mode 100644 index 8dbb4eb1d99..00000000000 --- a/vhdbuilder/packer/test/test_pkg_name_extraction.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash -# Test that getPackageJSON + renovateTag extraction resolves correct package names -# for containerd and kubernetes-cri-tools across all supported OS variants. -# -# This is a local-only test script (not run in CI). ShellSpec integration was -# deferred because cse_helpers.sh has system dependencies that are hard to -# satisfy in the shellspec Docker environment. -# -# Usage: bash vhdbuilder/packer/test/test_pkg_name_extraction.sh - -set -eo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" - -# Source cse_helpers.sh for getPackageJSON -# Set defaults for variables that cse_helpers.sh expects -OS_VARIANT="${OS_VARIANT:-DEFAULT}" -if ! source "$REPO_ROOT/parts/linux/cloud-init/artifacts/cse_helpers.sh" 2>/dev/null || ! type getPackageJSON >/dev/null 2>&1; then - echo "Failed to source cse_helpers.sh: getPackageJSON is not defined" >&2 - exit 1 -fi - -COMPONENTS_FILEPATH="$REPO_ROOT/parts/common/components.json" - -# Helper: extract package name from renovateTag via getPackageJSON -# Uses sed instead of grep -P for macOS compatibility -extractPkgName() { - local packageJson="$1" - local os="$2" - local osVersion="$3" - local osVariant="${4:-DEFAULT}" - getPackageJSON "$packageJson" "$os" "$osVersion" "$osVariant" \ - | jq -r '.versionsV2[0].renovateTag // empty' \ - | sed -n 's/.*name=\([^,]*\).*/\1/p' \ - | xargs -} - -PASS=0 -FAIL=0 - -assertPkgName() { - local description="$1" - local expected="$2" - local actual="$3" - - if [ "$actual" = "$expected" ]; then - echo "✓ $description: got '$actual'" - PASS=$((PASS + 1)) - else - echo "✗ $description: expected '$expected', got '$actual'" - FAIL=$((FAIL + 1)) - fi -} - -# Load containerd package JSON -containerdPkg=$(jq -c '.Packages[] | select(.name == "containerd")' "$COMPONENTS_FILEPATH") -criToolsPkg=$(jq -c '.Packages[] | select(.name == "kubernetes-cri-tools")' "$COMPONENTS_FILEPATH") - -echo "=== containerd package name extraction ===" - -# Ubuntu variants -result=$(extractPkgName "$containerdPkg" "UBUNTU" "24.04") -assertPkgName "containerd on Ubuntu 24.04" "moby-containerd" "$result" - -result=$(extractPkgName "$containerdPkg" "UBUNTU" "22.04") -assertPkgName "containerd on Ubuntu 22.04" "moby-containerd" "$result" - -result=$(extractPkgName "$containerdPkg" "UBUNTU" "20.04") -assertPkgName "containerd on Ubuntu 20.04" "moby-containerd" "$result" - -# Azure Linux -result=$(extractPkgName "$containerdPkg" "AZURELINUX" "3.0") -assertPkgName "containerd on Azure Linux 3.0" "containerd2" "$result" - -# Mariner -result=$(extractPkgName "$containerdPkg" "MARINER" "2.0" "DEFAULT") -assertPkgName "containerd on Mariner 2.0" "moby-containerd" "$result" - -echo "" -echo "=== kubernetes-cri-tools package name extraction ===" - -result=$(extractPkgName "$criToolsPkg" "UBUNTU" "24.04") -assertPkgName "cri-tools on Ubuntu 24.04" "kubernetes-cri-tools" "$result" - -result=$(extractPkgName "$criToolsPkg" "UBUNTU" "22.04") -assertPkgName "cri-tools on Ubuntu 22.04" "kubernetes-cri-tools" "$result" - -result=$(extractPkgName "$criToolsPkg" "AZURELINUX" "3.0") -assertPkgName "cri-tools on Azure Linux 3.0" "kubernetes-cri-tools" "$result" - -echo "" -echo "=== Results: $PASS passed, $FAIL failed ===" -if [ "$FAIL" -gt 0 ]; then - exit 1 -fi From 5d51f7c5dffe2d96098ccd68a26ada35e0357f8f Mon Sep 17 00:00:00 2001 From: Abigail Liang Date: Mon, 20 Jul 2026 15:23:14 -0700 Subject: [PATCH 07/11] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- vhdbuilder/packer/test/linux-vhd-content-test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index 1a033cc74e4..e87a0ad8c87 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -2018,7 +2018,7 @@ testCriCtl() { # Strict match: verify the full deb/rpm package version matches components.json if [ -z "$installedPackageName" ]; then - err "$test" "could not resolve package name from components.json renovateTag" + err "$test" "installed package name was not provided" return 1 fi assertPackageVersion "$test" "$installedPackageName" "$expectedVersion" || return 1 @@ -2053,7 +2053,7 @@ testContainerd() { # Strict match: verify the full deb/rpm package version matches components.json if [ -z "$installedPackageName" ]; then - err "$test" "could not resolve package name from components.json renovateTag" + err "$test" "installed package name was not provided" return 1 fi assertPackageVersion "$test" "$installedPackageName" "$expectedVersion" || return 1 From a1001d80d6f1e5c9d294b7fa09aa77c712eb5520 Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Mon, 20 Jul 2026 15:49:06 -0700 Subject: [PATCH 08/11] test: add ShellSpec tests for assertPackageVersion helper function --- .../test/assert_package_version_spec.sh | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 spec/vhdbuilder/packer/test/assert_package_version_spec.sh diff --git a/spec/vhdbuilder/packer/test/assert_package_version_spec.sh b/spec/vhdbuilder/packer/test/assert_package_version_spec.sh new file mode 100644 index 00000000000..88b620ff52d --- /dev/null +++ b/spec/vhdbuilder/packer/test/assert_package_version_spec.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# shellcheck disable=SC2329 + +# ShellSpec tests for assertPackageVersion helper function in linux-vhd-content-test.sh. +# Covers: dpkg path, rpm path, package-not-installed path, version mismatch path, and epoch stripping. + +Describe 'assertPackageVersion helper function' + # Source the functions by writing extracted content to a temp file (avoids eval expansion issues with ${Status}/${Version}). + BeforeAll ' + _tmpfunc=$(mktemp) + sed -n "/^err()/,/^}/p" "./vhdbuilder/packer/test/linux-vhd-content-test.sh" > "$_tmpfunc" + sed -n "/^assertPackageVersion()/,/^}/p" "./vhdbuilder/packer/test/linux-vhd-content-test.sh" >> "$_tmpfunc" + . "$_tmpfunc" + rm -f "$_tmpfunc" + ' + + Describe 'dpkg path (deb-based systems)' + It 'succeeds when installed deb version matches expected version' + # Mock dpkg-query to simulate moby-containerd 2.3.2-ubuntu24.04u2 installed + dpkg-query() { + case "$3" in + '${Status}') echo "install ok installed" ;; + '${Version}') echo "2.3.2-ubuntu24.04u2" ;; + esac + } + When call assertPackageVersion "testContainerd" "moby-containerd" "2.3.2-ubuntu24.04u2" + The status should equal 0 + The output should include "matches expected" + End + + It 'fails when installed deb version does not match expected version' + dpkg-query() { + case "$3" in + '${Status}') echo "install ok installed" ;; + '${Version}') echo "2.3.2-ubuntu24.04u2" ;; + esac + } + When call assertPackageVersion "testContainerd" "moby-containerd" "2.3.2-ubuntu24.04u1" + The status should equal 1 + The error should include "does not match expected" + End + + It 'strips epoch prefix from dpkg version before comparison' + dpkg-query() { + case "$3" in + '${Status}') echo "install ok installed" ;; + '${Version}') echo "1:1.7.33-ubuntu22.04u1" ;; + esac + } + When call assertPackageVersion "testContainerd" "moby-containerd" "1.7.33-ubuntu22.04u1" + The status should equal 0 + End + End + + Describe 'rpm path (RPM-based systems)' + It 'succeeds when installed rpm version matches expected version' + # Mock: dpkg-query not available, rpm available + dpkg-query() { return 1; } + command() { + case "$2" in + dpkg-query) return 1 ;; + rpm) return 0 ;; + esac + } + rpm() { + case "$1" in + -q) + if [ "$2" = "--queryformat" ]; then + echo "2.2.4-4.azl3" + else + return 0 + fi + ;; + esac + } + When call assertPackageVersion "testContainerd" "containerd2" "2.2.4-4.azl3" + The status should equal 0 + End + + It 'fails when installed rpm version does not match expected version' + dpkg-query() { return 1; } + command() { + case "$2" in + dpkg-query) return 1 ;; + rpm) return 0 ;; + esac + } + rpm() { + case "$1" in + -q) + if [ "$2" = "--queryformat" ]; then + echo "2.2.4-3.azl3" + else + return 0 + fi + ;; + esac + } + When call assertPackageVersion "testContainerd" "containerd2" "2.2.4-4.azl3" + The status should equal 1 + The error should include "does not match expected" + End + End + + Describe 'package not installed path' + It 'fails when package is not installed (neither dpkg nor rpm finds it)' + dpkg-query() { + case "$3" in + '${Status}') echo "unknown ok not-installed" ;; + esac + } + command() { + case "$2" in + dpkg-query) return 0 ;; + rpm) return 1 ;; + esac + } + When call assertPackageVersion "testContainerd" "moby-containerd" "2.3.2-ubuntu24.04u2" + The status should equal 1 + The error should include "is not installed" + End + End +End From a4872e5b0acdab4f91caf37eafdbd0b48afe7663 Mon Sep 17 00:00:00 2001 From: Abigail Liang Date: Mon, 20 Jul 2026 15:51:08 -0700 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- vhdbuilder/packer/test/linux-vhd-content-test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index e87a0ad8c87..00e7a8dbf77 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -2005,7 +2005,7 @@ assertPackageVersion() { } testCriCtl() { - expectedVersion="${1}" + local expectedVersion="${1}" local installedPackageName="${2}" local test="testCriCtl" echo "$test: Start" @@ -2040,7 +2040,7 @@ testCriCtl() { } testContainerd() { - expectedVersion="${1}" + local expectedVersion="${1}" local installedPackageName="${2}" local test="testContainerd" echo "$test: Start" From d0f895dc95a49330cf423c3154b9ffe40919474f Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Mon, 20 Jul 2026 16:13:46 -0700 Subject: [PATCH 10/11] feat: refactor package version assertion logic and add shared helper functions --- .../test/assert_package_version_spec.sh | 23 +++++------- vhdbuilder/packer/test/lib/package_helpers.sh | 37 +++++++++++++++++++ .../packer/test/linux-vhd-content-test.sh | 32 +--------------- 3 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 vhdbuilder/packer/test/lib/package_helpers.sh diff --git a/spec/vhdbuilder/packer/test/assert_package_version_spec.sh b/spec/vhdbuilder/packer/test/assert_package_version_spec.sh index 88b620ff52d..58c842fca16 100644 --- a/spec/vhdbuilder/packer/test/assert_package_version_spec.sh +++ b/spec/vhdbuilder/packer/test/assert_package_version_spec.sh @@ -1,22 +1,18 @@ #!/bin/bash -# shellcheck disable=SC2329 +# shellcheck disable=SC2016,SC2329 -# ShellSpec tests for assertPackageVersion helper function in linux-vhd-content-test.sh. -# Covers: dpkg path, rpm path, package-not-installed path, version mismatch path, and epoch stripping. +# ShellSpec tests for assertPackageVersion helper function. +# The function under test lives in vhdbuilder/packer/test/lib/package_helpers.sh. + +# Source the shared helper (provides err + assertPackageVersion). +# shellcheck source=../../../../vhdbuilder/packer/test/lib/package_helpers.sh +. "./vhdbuilder/packer/test/lib/package_helpers.sh" Describe 'assertPackageVersion helper function' - # Source the functions by writing extracted content to a temp file (avoids eval expansion issues with ${Status}/${Version}). - BeforeAll ' - _tmpfunc=$(mktemp) - sed -n "/^err()/,/^}/p" "./vhdbuilder/packer/test/linux-vhd-content-test.sh" > "$_tmpfunc" - sed -n "/^assertPackageVersion()/,/^}/p" "./vhdbuilder/packer/test/linux-vhd-content-test.sh" >> "$_tmpfunc" - . "$_tmpfunc" - rm -f "$_tmpfunc" - ' Describe 'dpkg path (deb-based systems)' It 'succeeds when installed deb version matches expected version' - # Mock dpkg-query to simulate moby-containerd 2.3.2-ubuntu24.04u2 installed + command() { return 0; } dpkg-query() { case "$3" in '${Status}') echo "install ok installed" ;; @@ -29,6 +25,7 @@ Describe 'assertPackageVersion helper function' End It 'fails when installed deb version does not match expected version' + command() { return 0; } dpkg-query() { case "$3" in '${Status}') echo "install ok installed" ;; @@ -41,6 +38,7 @@ Describe 'assertPackageVersion helper function' End It 'strips epoch prefix from dpkg version before comparison' + command() { return 0; } dpkg-query() { case "$3" in '${Status}') echo "install ok installed" ;; @@ -54,7 +52,6 @@ Describe 'assertPackageVersion helper function' Describe 'rpm path (RPM-based systems)' It 'succeeds when installed rpm version matches expected version' - # Mock: dpkg-query not available, rpm available dpkg-query() { return 1; } command() { case "$2" in diff --git a/vhdbuilder/packer/test/lib/package_helpers.sh b/vhdbuilder/packer/test/lib/package_helpers.sh new file mode 100644 index 00000000000..323f039c473 --- /dev/null +++ b/vhdbuilder/packer/test/lib/package_helpers.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Shared helper functions for VHD content testing. +# Sourced by both linux-vhd-content-test.sh (runtime on VM) and ShellSpec unit tests. + +# shellcheck disable=SC2016 + +err() { + echo "$1:Error: $2" >>/dev/stderr +} + +# assertPackageVersion verifies that the installed deb/rpm package version matches +# the expected full version string from components.json (including hotfix suffix). +# This catches drift between what the package manager installs and what components.json +# specifies at VHD build time rather than in e2e. +assertPackageVersion() { + local test="$1" + local packageName="$2" + local expectedVersion="$3" + + local installedVersion="" + if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' "$packageName" 2>/dev/null | grep -q "install ok installed"; then + # dpkg versions may include an epoch prefix (e.g. "1:..."); strip it for comparison with components.json. + installedVersion=$(dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null | sed 's/^[0-9]*://') + elif command -v rpm >/dev/null 2>&1 && rpm -q "$packageName" >/dev/null 2>&1; then + installedVersion=$(rpm -q --queryformat '%{VERSION}-%{RELEASE}' "$packageName" 2>/dev/null) + else + err "$test" "$packageName is not installed" + return 1 + fi + + echo "$test: checking if installed $packageName version '$installedVersion' matches expected '$expectedVersion'" + if [ "$installedVersion" != "$expectedVersion" ]; then + err "$test" "installed $packageName version '$installedVersion' does not match expected '$expectedVersion' from components.json" + return 1 + fi + return 0 +} diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index 00e7a8dbf77..e4c9b2b9ad4 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -29,9 +29,7 @@ CLOUD_INIT_LOG_MSG_IGNORE_LIST=( "Command ['hostname', '-f']" ) -err() { - echo "$1:Error: $2" >>/dev/stderr -} +source "${THIS_DIR}/lib/package_helpers.sh" # Clone the repo and checkout the branch provided. # Simply clone with just the branch doesn't work for pull requests, but this technique works @@ -1976,34 +1974,6 @@ testAKSNodeControllerService() { echo "$test:Finish" } -# assertPackageVersion verifies that the installed deb/rpm package version matches -# the expected full version string from components.json (including hotfix suffix). -# This catches drift between what the package manager installs and what components.json -# specifies at VHD build time rather than in e2e. -assertPackageVersion() { - local test="$1" - local packageName="$2" - local expectedVersion="$3" - - local installedVersion="" - if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' "$packageName" 2>/dev/null | grep -q "install ok installed"; then - # dpkg versions may include an epoch prefix (e.g. "1:..."); strip it for comparison with components.json. - installedVersion=$(dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null | sed 's/^[0-9]*://') - elif command -v rpm >/dev/null 2>&1 && rpm -q "$packageName" >/dev/null 2>&1; then - installedVersion=$(rpm -q --queryformat '%{VERSION}-%{RELEASE}' "$packageName" 2>/dev/null) - else - err "$test" "$packageName is not installed" - return 1 - fi - - echo "$test: checking if installed $packageName version '$installedVersion' matches expected '$expectedVersion'" - if [ "$installedVersion" != "$expectedVersion" ]; then - err "$test" "installed $packageName version '$installedVersion' does not match expected '$expectedVersion' from components.json" - return 1 - fi - return 0 -} - testCriCtl() { local expectedVersion="${1}" local installedPackageName="${2}" From 1bd9c962bf308dee0f2b556632ee7ba58889e798 Mon Sep 17 00:00:00 2001 From: Abigailliang Date: Mon, 20 Jul 2026 16:38:52 -0700 Subject: [PATCH 11/11] refactor: move assertPackageVersion function into linux-vhd-content-test.sh and remove redundant test files --- .../test/assert_package_version_spec.sh | 120 ------------------ vhdbuilder/packer/test/lib/package_helpers.sh | 37 ------ .../packer/test/linux-vhd-content-test.sh | 37 +++++- 3 files changed, 34 insertions(+), 160 deletions(-) delete mode 100644 spec/vhdbuilder/packer/test/assert_package_version_spec.sh delete mode 100644 vhdbuilder/packer/test/lib/package_helpers.sh diff --git a/spec/vhdbuilder/packer/test/assert_package_version_spec.sh b/spec/vhdbuilder/packer/test/assert_package_version_spec.sh deleted file mode 100644 index 58c842fca16..00000000000 --- a/spec/vhdbuilder/packer/test/assert_package_version_spec.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -# shellcheck disable=SC2016,SC2329 - -# ShellSpec tests for assertPackageVersion helper function. -# The function under test lives in vhdbuilder/packer/test/lib/package_helpers.sh. - -# Source the shared helper (provides err + assertPackageVersion). -# shellcheck source=../../../../vhdbuilder/packer/test/lib/package_helpers.sh -. "./vhdbuilder/packer/test/lib/package_helpers.sh" - -Describe 'assertPackageVersion helper function' - - Describe 'dpkg path (deb-based systems)' - It 'succeeds when installed deb version matches expected version' - command() { return 0; } - dpkg-query() { - case "$3" in - '${Status}') echo "install ok installed" ;; - '${Version}') echo "2.3.2-ubuntu24.04u2" ;; - esac - } - When call assertPackageVersion "testContainerd" "moby-containerd" "2.3.2-ubuntu24.04u2" - The status should equal 0 - The output should include "matches expected" - End - - It 'fails when installed deb version does not match expected version' - command() { return 0; } - dpkg-query() { - case "$3" in - '${Status}') echo "install ok installed" ;; - '${Version}') echo "2.3.2-ubuntu24.04u2" ;; - esac - } - When call assertPackageVersion "testContainerd" "moby-containerd" "2.3.2-ubuntu24.04u1" - The status should equal 1 - The error should include "does not match expected" - End - - It 'strips epoch prefix from dpkg version before comparison' - command() { return 0; } - dpkg-query() { - case "$3" in - '${Status}') echo "install ok installed" ;; - '${Version}') echo "1:1.7.33-ubuntu22.04u1" ;; - esac - } - When call assertPackageVersion "testContainerd" "moby-containerd" "1.7.33-ubuntu22.04u1" - The status should equal 0 - End - End - - Describe 'rpm path (RPM-based systems)' - It 'succeeds when installed rpm version matches expected version' - dpkg-query() { return 1; } - command() { - case "$2" in - dpkg-query) return 1 ;; - rpm) return 0 ;; - esac - } - rpm() { - case "$1" in - -q) - if [ "$2" = "--queryformat" ]; then - echo "2.2.4-4.azl3" - else - return 0 - fi - ;; - esac - } - When call assertPackageVersion "testContainerd" "containerd2" "2.2.4-4.azl3" - The status should equal 0 - End - - It 'fails when installed rpm version does not match expected version' - dpkg-query() { return 1; } - command() { - case "$2" in - dpkg-query) return 1 ;; - rpm) return 0 ;; - esac - } - rpm() { - case "$1" in - -q) - if [ "$2" = "--queryformat" ]; then - echo "2.2.4-3.azl3" - else - return 0 - fi - ;; - esac - } - When call assertPackageVersion "testContainerd" "containerd2" "2.2.4-4.azl3" - The status should equal 1 - The error should include "does not match expected" - End - End - - Describe 'package not installed path' - It 'fails when package is not installed (neither dpkg nor rpm finds it)' - dpkg-query() { - case "$3" in - '${Status}') echo "unknown ok not-installed" ;; - esac - } - command() { - case "$2" in - dpkg-query) return 0 ;; - rpm) return 1 ;; - esac - } - When call assertPackageVersion "testContainerd" "moby-containerd" "2.3.2-ubuntu24.04u2" - The status should equal 1 - The error should include "is not installed" - End - End -End diff --git a/vhdbuilder/packer/test/lib/package_helpers.sh b/vhdbuilder/packer/test/lib/package_helpers.sh deleted file mode 100644 index 323f039c473..00000000000 --- a/vhdbuilder/packer/test/lib/package_helpers.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -# Shared helper functions for VHD content testing. -# Sourced by both linux-vhd-content-test.sh (runtime on VM) and ShellSpec unit tests. - -# shellcheck disable=SC2016 - -err() { - echo "$1:Error: $2" >>/dev/stderr -} - -# assertPackageVersion verifies that the installed deb/rpm package version matches -# the expected full version string from components.json (including hotfix suffix). -# This catches drift between what the package manager installs and what components.json -# specifies at VHD build time rather than in e2e. -assertPackageVersion() { - local test="$1" - local packageName="$2" - local expectedVersion="$3" - - local installedVersion="" - if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' "$packageName" 2>/dev/null | grep -q "install ok installed"; then - # dpkg versions may include an epoch prefix (e.g. "1:..."); strip it for comparison with components.json. - installedVersion=$(dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null | sed 's/^[0-9]*://') - elif command -v rpm >/dev/null 2>&1 && rpm -q "$packageName" >/dev/null 2>&1; then - installedVersion=$(rpm -q --queryformat '%{VERSION}-%{RELEASE}' "$packageName" 2>/dev/null) - else - err "$test" "$packageName is not installed" - return 1 - fi - - echo "$test: checking if installed $packageName version '$installedVersion' matches expected '$expectedVersion'" - if [ "$installedVersion" != "$expectedVersion" ]; then - err "$test" "installed $packageName version '$installedVersion' does not match expected '$expectedVersion' from components.json" - return 1 - fi - return 0 -} diff --git a/vhdbuilder/packer/test/linux-vhd-content-test.sh b/vhdbuilder/packer/test/linux-vhd-content-test.sh index e4c9b2b9ad4..77c3ba0978c 100644 --- a/vhdbuilder/packer/test/linux-vhd-content-test.sh +++ b/vhdbuilder/packer/test/linux-vhd-content-test.sh @@ -29,7 +29,38 @@ CLOUD_INIT_LOG_MSG_IGNORE_LIST=( "Command ['hostname', '-f']" ) -source "${THIS_DIR}/lib/package_helpers.sh" +err() { + echo "$1:Error: $2" >>/dev/stderr +} + +# assertPackageVersion verifies that the installed deb/rpm package version matches +# the expected full version string from components.json (including hotfix suffix). +# This catches drift between what the package manager installs and what components.json +# specifies at VHD build time rather than in e2e. +# shellcheck disable=SC2016 +assertPackageVersion() { + local test="$1" + local packageName="$2" + local expectedVersion="$3" + + local installedVersion="" + if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' "$packageName" 2>/dev/null | grep -q "install ok installed"; then + # dpkg versions may include an epoch prefix (e.g. "1:..."); strip it for comparison with components.json. + installedVersion=$(dpkg-query -W -f='${Version}' "$packageName" 2>/dev/null | sed 's/^[0-9]*://') + elif command -v rpm >/dev/null 2>&1 && rpm -q "$packageName" >/dev/null 2>&1; then + installedVersion=$(rpm -q --queryformat '%{VERSION}-%{RELEASE}' "$packageName" 2>/dev/null) + else + err "$test" "$packageName is not installed" + return 1 + fi + + echo "$test: checking if installed $packageName version '$installedVersion' matches expected '$expectedVersion'" + if [ "$installedVersion" != "$expectedVersion" ]; then + err "$test" "installed $packageName version '$installedVersion' does not match expected '$expectedVersion' from components.json" + return 1 + fi + return 0 +} # Clone the repo and checkout the branch provided. # Simply clone with just the branch doesn't work for pull requests, but this technique works @@ -2035,8 +2066,8 @@ testContainerd() { containerd_version=$(containerd --version) # For containerd (v1): containerd github.com/containerd/containerd 1.6.26 # For containerd (v2): containerd github.com/containerd/containerd/v2 2.0.0 - containerd_version=$(echo $containerd_version | cut -d' ' -f3) - containerd_version=$(echo "$containerd_version" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+') + # Extract the semver from anywhere in the output (works for both v1 and v2). + containerd_version=$(echo "$containerd_version" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1) echo "$test: checking if containerd binary version is $expectedMajorMinorPatch" if [ "$containerd_version" != "$expectedMajorMinorPatch" ]; then err "$test" "containerd binary version is not $expectedMajorMinorPatch, instead it is $containerd_version"