Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions scripts/installer-scenarios.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
#
# Runs INSIDE a distro container, driven by test-smallstep-agent-installer.sh.
# Exercises the installer twice on the same box and prints one RESULT line per
# scenario for the harness to collect:
#
# install fresh install of the latest stable release
# upgrade previous stable -> latest, via the installer, checking that the
# package's post-install scriptlet actually ran on the upgrade
#
# Env (set by the harness): STEP_AGENT_TEAM, LATEST_VERSION, UPGRADE_FROM.

set -u

INSTALLER=/src/smallstep-agent-install.sh

# The probe is a side effect only the scriptlet produces: postinst writes the
# tss SupplementaryGroups drop-in iff group tss exists. Anything the package
# manager restores on its own (the step-agent user, /run/step-agent, ...) is
# useless here -- pacman's sysusers/tmpfiles hooks recreate those on every
# transaction and made an earlier version of this check pass on a broken
# package. The drop-in is touched by no libalpm hook, dpkg trigger or rpm
# filetrigger. The group is created up front so the probe applies on distros
# whose dependency set does not pull it in (deb/rpm do not depend on tpm2-tss).
PROBE=/etc/systemd/system/step-agent.service.d/tss.conf
getent group tss >/dev/null 2>&1 || groupadd tss

result() { echo "RESULT $1 $2"; }

installed_version() {
local v=""
if command -v pacman >/dev/null 2>&1; then
v=$(pacman -Q step-agent 2>/dev/null | awk '{print $2}')
elif command -v dpkg-query >/dev/null 2>&1; then
v=$(dpkg-query -W -f '${Version}' step-agent 2>/dev/null)
elif command -v rpm >/dev/null 2>&1; then
v=$(rpm -q --qf '%{VERSION}' step-agent 2>/dev/null)
fi
# Drop the package release suffix: 0.69.2-1 -> 0.69.2.
echo "${v%%-*}"
}

# Put the previous stable release in place the way a customer who installed
# it back then would have it, using the repo the installer just configured.
# Every stable version stays available in the apt and yum repos and in the
# versioned manifest tree, so the previous release is always reachable. The
# package release is always 1 (packageRelease in smallstep/agent's
# .goreleaser.yml).
downgrade_to() {
local v="$1"
if command -v pacman >/dev/null 2>&1; then
local pkg
pkg="step-agent-${v}-1-$(uname -m).pkg.tar.zst"
curl -fsSL -o "/tmp/${pkg}" "https://packages.smallstep.com/stable/step-agent/linux/${v}/${pkg}" \
&& pacman -U --noconfirm "/tmp/${pkg}"
elif command -v apt-get >/dev/null 2>&1; then
apt-get install -y --allow-downgrades "step-agent=${v}-1"
elif command -v dnf >/dev/null 2>&1; then
dnf downgrade -y "step-agent-${v}"
else
echo "no supported package manager found" >&2
return 1
fi
}

# --- install -----------------------------------------------------------------
echo "### scenario: install (fresh, expecting ${LATEST_VERSION})"
if ! "$INSTALLER"; then
result install FAIL
exit 1
fi
have=$(installed_version)
if [[ "$have" != "$LATEST_VERSION" ]]; then
echo "installed ${have}, expected ${LATEST_VERSION}" >&2
result install FAIL
exit 1
fi
result install PASS

# Older packages (0.68.0 and before) call systemctl unguarded from their
# scriptlets and fail in a container that is not booted with systemd -- which
# aborts the dpkg configure step and leaves nothing to upgrade from. Current
# packages check for /run/systemd/system first. Laying down the previous
# release is only setup for the upgrade under test, so a no-op systemctl is
# put in place for that step alone and removed again before the installer
# runs the real upgrade. A container limitation, not something real hosts hit.
with_noop_systemctl() {
local real
real=$(command -v systemctl 2>/dev/null || echo /usr/bin/systemctl)
[[ -e "$real" ]] && mv "$real" "${real}.real"
printf '#!/bin/sh\nexit 0\n' > "$real" && chmod 0755 "$real"
local rc=0
"$@" || rc=$?
rm -f "$real"
[[ -e "${real}.real" ]] && mv "${real}.real" "$real"
return "$rc"
}

# --- upgrade -----------------------------------------------------------------
echo "### scenario: upgrade (${UPGRADE_FROM} -> ${LATEST_VERSION})"
if ! with_noop_systemctl downgrade_to "$UPGRADE_FROM"; then
echo "could not install previous release ${UPGRADE_FROM}" >&2
result upgrade FAIL
exit 1
fi
have=$(installed_version)
if [[ "$have" != "$UPGRADE_FROM" ]]; then
echo "installed ${have} after downgrade, expected ${UPGRADE_FROM}" >&2
result upgrade FAIL
exit 1
fi

rm -f "$PROBE"

if ! "$INSTALLER"; then
result upgrade FAIL
exit 1
fi
have=$(installed_version)
if [[ "$have" != "$LATEST_VERSION" ]]; then
echo "installed ${have} after upgrade, expected ${LATEST_VERSION}" >&2
result upgrade FAIL
exit 1
fi
if [[ ! -f "$PROBE" ]]; then
echo "${PROBE} missing after upgrade: the package's install scriptlet did not run" >&2
result upgrade FAIL
exit 1
fi
result upgrade PASS
67 changes: 56 additions & 11 deletions scripts/test-smallstep-agent-installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,38 @@ set -e
# are each always covered — :latest silently drifts to the newest major.
DISTRO_CONTAINER_LIST=(fedora:latest redhat/ubi9:latest quay.io/centos/centos:stream9 almalinux:9 almalinux:10 rockylinux/rockylinux:9.3.20231119 debian:latest ubuntu:latest archlinux:base)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
MANIFEST_URL="https://packages.smallstep.com/stable/step-agent/linux/index.json"

# Narrow the run to specific images, e.g. DISTROS="archlinux:base debian:latest"
if [[ -n "${DISTROS:-}" ]]; then
read -ra DISTRO_CONTAINER_LIST <<< "${DISTROS}"
fi

# Each container runs two scenarios (see installer-scenarios.sh): a fresh
# install of the latest stable release, then an upgrade from the previous
# stable release to it. Both versions come from the manifest tree so the
# suite tracks releases on its own; UPGRADE_FROM overrides the starting point,
# e.g. UPGRADE_FROM=0.67.3 to reproduce a specific customer's upgrade path.
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 is required to read ${MANIFEST_URL}" >&2
exit 2
fi
read -r LATEST_VERSION PREVIOUS_VERSION < <(curl -fsSL "${MANIFEST_URL}" | python3 -c '
import json, sys
m = json.load(sys.stdin)
latest = m["latest_version"]
# versions[] is newest first and may include -rc builds, which never become
# latest and are not what customers upgrade from.
older = [v["version"] for v in m["versions"] if v["version"] != latest and "-rc" not in v["version"]]
print(latest, older[0] if older else "")
')
UPGRADE_FROM="${UPGRADE_FROM:-${PREVIOUS_VERSION}}"
if [[ -z "${LATEST_VERSION}" || -z "${UPGRADE_FROM}" ]]; then
echo "could not resolve latest/previous versions from ${MANIFEST_URL}" >&2
exit 2
fi
echo "Latest stable: ${LATEST_VERSION}; upgrade scenario starts from ${UPGRADE_FROM}"

TEST_REPORT=()
FAILURES=0

Expand All @@ -36,22 +62,41 @@ for DISTRO in "${DISTRO_CONTAINER_LIST[@]}"; do
# The installer calls tput, which needs TERM. Passing it explicitly means we
# don't have to allocate a TTY (`docker run -t`), which would break this
# harness under CI where stdin is not a terminal.
EXITCODE=0
docker run --rm \
#
# The scenario script reports each scenario on a "RESULT <name> PASS|FAIL"
# line; the container's output is streamed and those lines picked out of it,
# so a container that dies early simply reports fewer scenarios.
#
# The whole checkout is mounted rather than the two scripts individually: a
# single-file bind mount is pinned to the inode it was first mounted from, and
# Docker Desktop keeps serving that stale copy after the file is rewritten in
# place. A directory mount always reflects the current contents.
OUTPUT=$(docker run --rm \
--name "test-smallstep-agent-install-${DISTRO_NICKNAME}" \
-e STEP_AGENT_TEAM=foo \
-e DEBIAN_FRONTEND=noninteractive \
-e TERM=xterm \
-v "${SCRIPT_DIR}/../smallstep-agent-install.sh:/smallstep-agent-install.sh:Z" \
-e LATEST_VERSION="${LATEST_VERSION}" \
-e UPGRADE_FROM="${UPGRADE_FROM}" \
-v "${SCRIPT_DIR}/..:/src:ro,Z" \
"${DISTRO}" \
bash -c "${PRE_CMD}./smallstep-agent-install.sh" || EXITCODE=$?

if [[ "${EXITCODE}" -eq 0 ]]; then
TEST_REPORT+=("${DISTRO}: Passed!")
else
TEST_REPORT+=("${DISTRO}: Failed! (exit ${EXITCODE})")
FAILURES=$((FAILURES + 1))
fi
bash -c "${PRE_CMD}/src/scripts/installer-scenarios.sh" 2>&1 | tee /dev/stderr) || true

for SCENARIO in install upgrade; do
case "${SCENARIO}" in
install) LABEL="install ${LATEST_VERSION}" ;;
upgrade) LABEL="upgrade ${UPGRADE_FROM} -> ${LATEST_VERSION}" ;;
esac
if grep -qx "RESULT ${SCENARIO} PASS" <<< "${OUTPUT}"; then
TEST_REPORT+=("${DISTRO} ${LABEL}: Passed!")
elif grep -qx "RESULT ${SCENARIO} FAIL" <<< "${OUTPUT}"; then
TEST_REPORT+=("${DISTRO} ${LABEL}: Failed!")
FAILURES=$((FAILURES + 1))
else
TEST_REPORT+=("${DISTRO} ${LABEL}: Failed! (no result; container exited early)")
FAILURES=$((FAILURES + 1))
fi
done
done

echo ""
Expand Down