diff --git a/scripts/installer-scenarios.sh b/scripts/installer-scenarios.sh new file mode 100755 index 0000000..522d413 --- /dev/null +++ b/scripts/installer-scenarios.sh @@ -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 diff --git a/scripts/test-smallstep-agent-installer.sh b/scripts/test-smallstep-agent-installer.sh index 0736028..314fd68 100755 --- a/scripts/test-smallstep-agent-installer.sh +++ b/scripts/test-smallstep-agent-installer.sh @@ -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 @@ -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 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 ""