mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-18 06:10:23 +00:00
The chart's agent.image.repository defaulted to ghcr.io/rcourtman/pulse-agent,
an image that has never been published. publish-docker.yml only pushes
rcourtman/pulse; the Dockerfile defines an agent_runtime stage that
*could* be published but it isn't, and commit da7969fb4 from earlier in
this session removed the corresponding pulse-agent attestation
expectations — a clear signal the separate agent image was intentionally
dropped without updating the chart. Customers running
`helm install pulse pulse/pulse --set agent.enabled=true` were silently
hitting ImagePullBackOff on the agent DaemonSet.
Route the chart through the main rcourtman/pulse image instead. To make
that work without per-arch chart overrides, the runtime stage in the
Dockerfile now creates an arch-resolved /usr/local/bin/pulse-agent
symlink to the right /opt/pulse/bin/pulse-agent-linux-{amd64,arm64,armv7}
binary. The chart's agent.command default is /usr/local/bin/pulse-agent,
which overrides the server ENTRYPOINT and runs the pod as a unified
agent on whichever arch the node provides. agent.yaml renders the
command via toYaml so list values pass through cleanly.
KUBERNETES.md's DaemonSet example switches from the arch-hardcoded
/opt/pulse/bin/pulse-agent-linux-amd64 to the new arch-resolved path,
restoring multi-arch portability of the docs example.
validate-release.sh asserts the symlink exists, points at one of the
three supported Linux arch binaries, and is executable in the published
image. A new TestHelmAgentRuntimePointsAtRealImage pins the chart
defaults, the template wiring, the Dockerfile symlink, and the
validate-release.sh guard so the regression class can't quietly
resurface.
Governance: extend the helm-chart-release-runtime verification policy's
exact_files to include scripts/installtests/build_release_assets_test.go
(matching its existing pin set for related deployment-installability
policies); update the subsystem_lookup_test.py fixture that pins the
exact_files list; document the agent-image and pulse-agent symlink
contract in deployment-installability.md Extension Point 7.
Verified locally: `helm lint` passes; `helm template --set agent.enabled=true`
renders a DaemonSet with image rcourtman/pulse:6.0.0,
command ["/usr/local/bin/pulse-agent"], args ["--enable-docker", "--enable-host=false"].
End-to-end image build + agent DaemonSet smoke will run via helm_smoke
on the next release once rcourtman/pulse:6.0.0 is published.
675 lines
28 KiB
Bash
Executable file
675 lines
28 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
# Pulse Release Validation Script
|
|
# Comprehensive artifact validation to prevent missing files/binaries in releases
|
|
#
|
|
# Usage: ./scripts/validate-release.sh <pulse-version> [image] [release-dir] [--skip-docker]
|
|
# Example: ./scripts/validate-release.sh 4.26.2
|
|
# ./scripts/validate-release.sh 4.26.2 rcourtman/pulse:v4.26.2 release
|
|
# ./scripts/validate-release.sh 4.26.2 --skip-docker
|
|
|
|
set -euo pipefail
|
|
|
|
# Color output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
error() {
|
|
echo -e "${RED}[ERROR]${NC} $*" >&2
|
|
}
|
|
|
|
section() {
|
|
echo ""
|
|
echo -e "${BLUE}=== ${1} ===${NC}"
|
|
}
|
|
|
|
success() {
|
|
echo -e "${GREEN}[✓]${NC} $*"
|
|
}
|
|
|
|
info() {
|
|
echo -e "${BLUE}[INFO]${NC} $*"
|
|
}
|
|
|
|
warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $*"
|
|
}
|
|
|
|
with_network_blocked() {
|
|
# Drop outbound traffic inside container by adding a reject route; avoids needing elevated host perms.
|
|
# Caller supplies: container name, command...
|
|
local container="$1"
|
|
shift
|
|
docker exec "$container" sh -c "ip route add blackhole 0.0.0.0/0 || true" && "$@"
|
|
}
|
|
|
|
check_tar_entries_nonempty() {
|
|
local tarball="$1"
|
|
shift
|
|
for entry in "$@"; do
|
|
if ! tar -tzf "$tarball" "$entry" >/dev/null 2>&1; then
|
|
error "$(basename "$tarball") missing entry: $entry"
|
|
exit 1
|
|
fi
|
|
# Examine type; skip size enforcement for symlinks
|
|
local type
|
|
type=$(tar -tvf "$tarball" "$entry" 2>/dev/null | awk 'NR==1 {print substr($0,1,1)}')
|
|
if [ "$type" = "l" ]; then
|
|
continue
|
|
fi
|
|
local size
|
|
size=$(tar -xOf "$tarball" "$entry" 2>/dev/null | wc -c | tr -d '[:space:]')
|
|
if [ -z "$size" ] || [ "$size" -le 0 ]; then
|
|
error "$(basename "$tarball") missing or empty entry: $entry"
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
http_header_value() {
|
|
local header_name="$1"
|
|
local headers_file="$2"
|
|
awk -v needle="$(printf '%s' "$header_name" | tr '[:upper:]' '[:lower:]')" '
|
|
BEGIN { value = "" }
|
|
{
|
|
line = $0
|
|
sub(/\r$/, "", line)
|
|
sub(/^[[:space:]]+/, "", line)
|
|
lower = tolower(line)
|
|
if (index(lower, needle ":") == 1) {
|
|
sub(/^[^:]*:[[:space:]]*/, "", line)
|
|
value = line
|
|
}
|
|
}
|
|
END { print value }
|
|
' "$headers_file"
|
|
}
|
|
|
|
validate_download_binary_headers() {
|
|
local file_path="$1"
|
|
local headers_path="$2"
|
|
local label="$3"
|
|
local checksum_header signature_header ssh_signature_header actual_checksum
|
|
|
|
checksum_header="$(http_header_value "X-Checksum-Sha256" "$headers_path")"
|
|
signature_header="$(http_header_value "X-Signature-Ed25519" "$headers_path")"
|
|
ssh_signature_header="$(http_header_value "X-Signature-SSHSIG" "$headers_path")"
|
|
|
|
if [ -z "$checksum_header" ]; then
|
|
error "${label} missing X-Checksum-Sha256 header"
|
|
exit 1
|
|
fi
|
|
if [ -z "$signature_header" ]; then
|
|
error "${label} missing X-Signature-Ed25519 header"
|
|
exit 1
|
|
fi
|
|
if [ -z "$ssh_signature_header" ]; then
|
|
error "${label} missing X-Signature-SSHSIG header"
|
|
exit 1
|
|
fi
|
|
|
|
actual_checksum="$(sha256sum "$file_path" | awk '{print $1}')"
|
|
if [ "$actual_checksum" != "$checksum_header" ]; then
|
|
error "${label} checksum header mismatch: expected ${checksum_header}, got ${actual_checksum}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_download_script_headers() {
|
|
local headers_path="$1"
|
|
local label="$2"
|
|
local signature_header ssh_signature_header
|
|
|
|
signature_header="$(http_header_value "X-Signature-Ed25519" "$headers_path")"
|
|
ssh_signature_header="$(http_header_value "X-Signature-SSHSIG" "$headers_path")"
|
|
|
|
if [ -z "$signature_header" ]; then
|
|
error "${label} missing X-Signature-Ed25519 header"
|
|
exit 1
|
|
fi
|
|
if [ -z "$ssh_signature_header" ]; then
|
|
error "${label} missing X-Signature-SSHSIG header"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
if [ $# -lt 1 ]; then
|
|
error "Usage: $0 <pulse-version> [image] [release-dir] [--skip-docker]"
|
|
exit 1
|
|
fi
|
|
|
|
PULSE_VERSION=$1
|
|
PULSE_TAG="v${PULSE_VERSION}"
|
|
SKIP_DOCKER=false
|
|
|
|
# Parse arguments
|
|
shift
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--skip-docker)
|
|
SKIP_DOCKER=true
|
|
shift
|
|
;;
|
|
*)
|
|
if [ -z "${IMAGE:-}" ]; then
|
|
IMAGE="$1"
|
|
elif [ -z "${RELEASE_DIR:-}" ]; then
|
|
RELEASE_DIR="$1"
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Set defaults
|
|
IMAGE=${IMAGE:-"rcourtman/pulse:${PULSE_TAG}"}
|
|
RELEASE_DIR=${RELEASE_DIR:-"release"}
|
|
|
|
# Validate prerequisites
|
|
if [ "$SKIP_DOCKER" = false ]; then
|
|
command -v docker >/dev/null || { error "docker is required (use --skip-docker to skip Docker validation)"; exit 1; }
|
|
fi
|
|
[ -d "$RELEASE_DIR" ] || { error "release dir not found: $RELEASE_DIR"; exit 1; }
|
|
|
|
# Create temp directory for extractions
|
|
tmp_root=$(mktemp -d)
|
|
smoke_container=""
|
|
trap 'rm -rf "$tmp_root"; if [ -n "$smoke_container" ]; then docker rm -f "$smoke_container" >/dev/null 2>&1 || true; fi' EXIT
|
|
|
|
info "Validating Pulse $PULSE_TAG release artifacts"
|
|
info "Image: $IMAGE"
|
|
info "Release directory: $RELEASE_DIR"
|
|
echo ""
|
|
|
|
#=============================================================================
|
|
# DOCKER IMAGE VALIDATION
|
|
#=============================================================================
|
|
if [ "$SKIP_DOCKER" = false ]; then
|
|
info "=== Docker Image Validation ==="
|
|
|
|
# Validate VERSION file in container
|
|
info "Checking VERSION file in Docker image..."
|
|
docker run --rm --entrypoint /bin/sh -e EXPECTED_VERSION="$PULSE_VERSION" "$IMAGE" -c 'set -euo pipefail; for path in /VERSION /app/VERSION; do if [ -f "$path" ]; then actual=$(cat "$path" | tr -d "\r\n"); [ "$actual" = "$EXPECTED_VERSION" ] && exit 0 || { echo "VERSION mismatch at $path: expected=$EXPECTED_VERSION actual=$actual" >&2; exit 1; }; fi; done; echo "VERSION file not found in image" >&2; exit 1' || { error "VERSION file mismatch in Docker image"; exit 1; }
|
|
success "VERSION file correct: $PULSE_VERSION"
|
|
|
|
# Validate all required scripts exist and are executable
|
|
info "Checking installer/uninstaller scripts in /opt/pulse/scripts/..."
|
|
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/scripts; required="install-container-agent.sh install-docker.sh install.sh"; for f in $required; do [ -f "$f" ] || { echo "missing script $f" >&2; exit 1; }; case "$f" in *.sh|*.ps1) [ -x "$f" ] || { echo "$f not executable" >&2; exit 1; };; esac; done; echo "All scripts present and executable"' || { error "Script validation failed"; exit 1; }
|
|
success "All installer/uninstaller scripts present and executable"
|
|
|
|
# Validate all required binaries exist and are non-empty
|
|
info "Checking downloadable binaries in /opt/pulse/bin/..."
|
|
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; cd /opt/pulse/bin; required="pulse pulse-agent-linux-amd64 pulse-agent-linux-arm64 pulse-agent-linux-armv7 pulse-agent-linux-armv6 pulse-agent-linux-386 pulse-agent-darwin-amd64 pulse-agent-darwin-arm64 pulse-agent-windows-amd64.exe pulse-agent-windows-amd64 pulse-agent-windows-arm64.exe pulse-agent-windows-arm64 pulse-agent-windows-386.exe pulse-agent-windows-386 pulse-agent-freebsd-amd64 pulse-agent-freebsd-arm64"; for f in $required; do [ -e "$f" ] || { echo "missing binary $f" >&2; exit 1; }; [ -s "$f" ] || { echo "empty binary $f" >&2; exit 1; }; done; [ "$(readlink pulse-agent-windows-amd64)" = "pulse-agent-windows-amd64.exe" ] || { echo "unified agent windows amd64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-arm64)" = "pulse-agent-windows-arm64.exe" ] || { echo "unified agent windows arm64 symlink broken" >&2; exit 1; }; [ "$(readlink pulse-agent-windows-386)" = "pulse-agent-windows-386.exe" ] || { echo "unified agent windows 386 symlink broken" >&2; exit 1; }; echo "All binaries present"' || { error "Binary validation failed"; exit 1; }
|
|
success "All downloadable binaries present"
|
|
|
|
# Validate the arch-resolved /usr/local/bin/pulse-agent symlink. The helm
|
|
# chart's agent workload (and `docker run rcourtman/pulse --entrypoint
|
|
# /usr/local/bin/pulse-agent`) depend on this path; without it the chart
|
|
# defaults to a non-existent image and `agent.enabled=true` hits
|
|
# ImagePullBackOff.
|
|
info "Validating /usr/local/bin/pulse-agent arch-resolved symlink..."
|
|
docker run --rm --entrypoint /bin/sh "$IMAGE" -c 'set -euo pipefail; [ -L /usr/local/bin/pulse-agent ] || { echo "/usr/local/bin/pulse-agent is missing or not a symlink" >&2; exit 1; }; target=$(readlink /usr/local/bin/pulse-agent); case "$target" in /opt/pulse/bin/pulse-agent-linux-amd64|/opt/pulse/bin/pulse-agent-linux-arm64|/opt/pulse/bin/pulse-agent-linux-armv7) : ;; *) echo "/usr/local/bin/pulse-agent points at unexpected target: $target" >&2; exit 1 ;; esac; [ -x "$target" ] || { echo "/usr/local/bin/pulse-agent target is not executable" >&2; exit 1; }; echo "pulse-agent symlink resolves to $target"' || { error "/usr/local/bin/pulse-agent validation failed"; exit 1; }
|
|
success "/usr/local/bin/pulse-agent symlink is arch-resolved and executable"
|
|
|
|
# Validate version embedding in Docker image binaries
|
|
info "Validating version embedding in Docker image binaries..."
|
|
|
|
# Pulse server binary
|
|
docker run --rm --entrypoint /app/pulse "$IMAGE" version 2>/dev/null | grep -Fx "Pulse $PULSE_TAG" >/dev/null || { error "Pulse server version mismatch"; exit 1; }
|
|
success "Pulse server version: $PULSE_TAG"
|
|
|
|
# Docker agent is embedded in the main pulse binary (check binary strings)
|
|
docker run --rm --entrypoint /bin/sh -e EXPECTED_TAG="$PULSE_TAG" "$IMAGE" -c 'set -euo pipefail; grep -aF "$EXPECTED_TAG" /app/pulse >/dev/null' || { error "Docker agent version string not found"; exit 1; }
|
|
success "Docker agent version embedded: $PULSE_TAG"
|
|
|
|
# Unified agent binary
|
|
docker run --rm --entrypoint /opt/pulse/bin/pulse-agent-linux-amd64 "$IMAGE" --version 2>/dev/null | grep -Fx "$PULSE_TAG" >/dev/null || { error "Unified agent version mismatch"; exit 1; }
|
|
success "Unified agent version: $PULSE_TAG"
|
|
|
|
# Smoke test download endpoints from a running container
|
|
info "Running download endpoint smoke tests..."
|
|
HOST_PORT=8765
|
|
SMOKE_CONTAINER="pulse-download-smoke-$$"
|
|
smoke_container="$SMOKE_CONTAINER"
|
|
|
|
docker run -d --rm \
|
|
--name "$SMOKE_CONTAINER" \
|
|
-p "$HOST_PORT:7655" \
|
|
-e PULSE_MOCK_MODE=true \
|
|
-e PULSE_ALLOW_DOCKER_UPDATES=true \
|
|
-e PULSE_AUTH_USER=admin \
|
|
-e PULSE_AUTH_PASS=admin \
|
|
"$IMAGE" >/dev/null
|
|
|
|
for i in $(seq 1 30); do
|
|
if curl -fsS "http://127.0.0.1:${HOST_PORT}/api/health" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
if [ "$i" -eq 30 ]; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Pulse container did not become healthy for download smoke tests"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
download_matrix=(
|
|
"linux amd64"
|
|
"linux arm64"
|
|
"linux armv7"
|
|
"linux armv6"
|
|
"linux 386"
|
|
"darwin amd64"
|
|
"darwin arm64"
|
|
"windows amd64"
|
|
"windows arm64"
|
|
"windows 386"
|
|
)
|
|
|
|
for script_name in install.sh install.ps1; do
|
|
url="http://127.0.0.1:${HOST_PORT}/download/${script_name}"
|
|
tmp_file=$(mktemp)
|
|
tmp_headers=$(mktemp)
|
|
if ! curl -fsS -D "$tmp_headers" -o "$tmp_file" "$url"; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Download failed for ${script_name}"
|
|
exit 1
|
|
fi
|
|
if [ ! -s "$tmp_file" ]; then
|
|
error "Downloaded empty ${script_name}"
|
|
exit 1
|
|
fi
|
|
validate_download_script_headers "$tmp_headers" "${script_name}"
|
|
rm -f "$tmp_file" "$tmp_headers"
|
|
done
|
|
success "Install script endpoints returned required signature headers"
|
|
|
|
for entry in "${download_matrix[@]}"; do
|
|
set -- $entry
|
|
platform=$1
|
|
arch=$2
|
|
url="http://127.0.0.1:${HOST_PORT}/download/pulse-agent?arch=${platform}-${arch}"
|
|
tmp_file=$(mktemp)
|
|
tmp_headers=$(mktemp)
|
|
if ! curl -fsS -D "$tmp_headers" -o "$tmp_file" "$url"; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Download failed for $platform/$arch"
|
|
exit 1
|
|
fi
|
|
if [ ! -s "$tmp_file" ]; then
|
|
error "Downloaded empty binary for $platform/$arch"
|
|
exit 1
|
|
fi
|
|
validate_download_binary_headers "$tmp_file" "$tmp_headers" "${platform}/${arch}"
|
|
rm -f "$tmp_file" "$tmp_headers"
|
|
done
|
|
success "Download endpoints returned binaries with checksum and signature headers for all platforms/architectures"
|
|
|
|
docker rm -f "$SMOKE_CONTAINER" >/dev/null 2>&1 || true
|
|
smoke_container=""
|
|
|
|
echo ""
|
|
|
|
# Offline self-heal check: run with no outbound network and confirm download endpoint still serves binaries
|
|
section "Offline self-heal smoke test"
|
|
SMOKE_CONTAINER="pulse-offline-smoke-$$"
|
|
smoke_container="$SMOKE_CONTAINER"
|
|
docker run -d --rm \
|
|
--name "$SMOKE_CONTAINER" \
|
|
--network none \
|
|
-e PULSE_MOCK_MODE=true \
|
|
-e PULSE_ALLOW_DOCKER_UPDATES=true \
|
|
-e PULSE_AUTH_USER=admin \
|
|
-e PULSE_AUTH_PASS=admin \
|
|
"$IMAGE" >/dev/null
|
|
|
|
for i in $(seq 1 30); do
|
|
if docker exec "$SMOKE_CONTAINER" wget -qO- http://127.0.0.1:7655/api/health >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
if [ "$i" -eq 30 ]; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Pulse container did not become healthy for offline smoke tests"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
offline_tmp=$(mktemp)
|
|
offline_headers=$(mktemp)
|
|
if ! docker exec "$SMOKE_CONTAINER" sh -c "headers=\$(mktemp); wget -qS -O- 'http://127.0.0.1:7655/download/pulse-agent?arch=linux-amd64' 2>\"\$headers\"; status=\$?; cat \"\$headers\" >&2; rm -f \"\$headers\"; exit \$status" > "$offline_tmp" 2> "$offline_headers"; then
|
|
docker logs "$SMOKE_CONTAINER" || true
|
|
error "Offline self-heal failed: download endpoint returned error with no outbound network"
|
|
exit 1
|
|
fi
|
|
if [ ! -s "$offline_tmp" ]; then
|
|
error "Offline self-heal failed: downloaded binary is empty"
|
|
exit 1
|
|
fi
|
|
validate_download_binary_headers "$offline_tmp" "$offline_headers" "offline linux/amd64"
|
|
rm -f "$offline_tmp" "$offline_headers"
|
|
success "Offline self-heal: download endpoint works with checksum and signature headers without outbound network"
|
|
|
|
docker rm -f "$SMOKE_CONTAINER" >/dev/null 2>&1 || true
|
|
smoke_container=""
|
|
|
|
echo ""
|
|
else
|
|
warn "=== Skipping Docker Image Validation (--skip-docker flag provided) ==="
|
|
echo ""
|
|
fi
|
|
|
|
#=============================================================================
|
|
# RELEASE TARBALL VALIDATION
|
|
#=============================================================================
|
|
info "=== Release Tarball Validation ==="
|
|
|
|
pushd "$RELEASE_DIR" >/dev/null
|
|
|
|
# Validate all expected release assets exist
|
|
info "Checking required release assets..."
|
|
# NOTE: Standalone binaries are NOT in GitHub releases
|
|
# They are only included in Docker images for /download/ endpoints
|
|
required_assets=(
|
|
"install.sh"
|
|
"checksums.txt"
|
|
"pulse-v${PULSE_VERSION}-release.sbom.spdx.json"
|
|
"pulse-v${PULSE_VERSION}.tar.gz"
|
|
"pulse-v${PULSE_VERSION}-linux-amd64.tar.gz"
|
|
"pulse-v${PULSE_VERSION}-linux-arm64.tar.gz"
|
|
"pulse-v${PULSE_VERSION}-linux-armv7.tar.gz"
|
|
"pulse-v${PULSE_VERSION}-linux-armv6.tar.gz"
|
|
"pulse-v${PULSE_VERSION}-linux-386.tar.gz"
|
|
"pulse-agent-v${PULSE_VERSION}-darwin-amd64.tar.gz"
|
|
"pulse-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz"
|
|
"pulse-agent-v${PULSE_VERSION}-windows-amd64.zip"
|
|
"pulse-agent-v${PULSE_VERSION}-windows-arm64.zip"
|
|
"pulse-agent-v${PULSE_VERSION}-windows-386.zip"
|
|
"pulse-agent-v${PULSE_VERSION}-freebsd-amd64.tar.gz"
|
|
"pulse-agent-v${PULSE_VERSION}-freebsd-arm64.tar.gz"
|
|
)
|
|
|
|
missing_count=0
|
|
for asset in "${required_assets[@]}"; do
|
|
if [ ! -e "$asset" ]; then
|
|
error "Missing release asset: $asset"
|
|
missing_count=$((missing_count + 1))
|
|
fi
|
|
done
|
|
|
|
if [ $missing_count -gt 0 ]; then
|
|
error "$missing_count required assets missing"
|
|
exit 1
|
|
fi
|
|
success "All ${#required_assets[@]} required release assets present"
|
|
|
|
# Validate published install.sh is the Pulse SERVER installer.
|
|
# Across v6 rc.1 → rc.5 the rendered AGENT installer was published here by
|
|
# mistake, silently breaking the LXC quickstart, the in-product Update Pulse
|
|
# button, and the pulse-auto-update.sh systemd timer for 30 days before anyone
|
|
# noticed. This guard pins the asset identity so it cannot drift back.
|
|
info "Validating install.sh is the Pulse server installer..."
|
|
install_sh_path="install.sh"
|
|
if [ ! -s "$install_sh_path" ]; then
|
|
error "install.sh is missing or empty"
|
|
exit 1
|
|
fi
|
|
if ! grep -qE '^# Pulse Installer Script' "$install_sh_path"; then
|
|
error "install.sh banner does not match the Pulse server installer"
|
|
error "If this fires, build-release.sh is publishing the wrong file (likely the agent installer)"
|
|
exit 1
|
|
fi
|
|
if ! grep -qE '^[[:space:]]*--version\)' "$install_sh_path"; then
|
|
error "install.sh is missing the --version arg handler — required by adapter_installsh, pulse-auto-update.sh, and the README quickstart"
|
|
exit 1
|
|
fi
|
|
if grep -q 'Pulse Unified Agent Installer' "$install_sh_path"; then
|
|
error "install.sh is the agent installer, not the server installer — releases must publish the root install.sh"
|
|
exit 1
|
|
fi
|
|
# Smoke: actually invoke `bash install.sh --help` and confirm it prints the
|
|
# server-installer help text. Catches parse-time syntax breakage and confirms
|
|
# the script is structurally executable, not just textually correct.
|
|
install_help_output=$(bash "$install_sh_path" --help 2>&1 || true)
|
|
if ! echo "$install_help_output" | grep -qF "Install specific version (e.g."; then
|
|
error "bash install.sh --help did not print the server installer's version-pinning help line"
|
|
error "Help output captured: $install_help_output"
|
|
exit 1
|
|
fi
|
|
success "install.sh is the Pulse server installer (handles --version, --help prints server help)"
|
|
|
|
# Validate tarball contents
|
|
section "Validating tarball contents"
|
|
tar_arches=(linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386)
|
|
unified_agent_entries=(
|
|
./bin/pulse-agent-linux-amd64
|
|
./bin/pulse-agent-linux-arm64
|
|
./bin/pulse-agent-linux-armv7
|
|
./bin/pulse-agent-linux-armv6
|
|
./bin/pulse-agent-linux-386
|
|
./bin/pulse-agent-darwin-amd64
|
|
./bin/pulse-agent-darwin-arm64
|
|
./bin/pulse-agent-windows-amd64.exe
|
|
./bin/pulse-agent-windows-arm64.exe
|
|
./bin/pulse-agent-windows-386.exe
|
|
./bin/pulse-agent-windows-amd64
|
|
./bin/pulse-agent-windows-arm64
|
|
./bin/pulse-agent-windows-386
|
|
./bin/pulse-agent-freebsd-amd64
|
|
./bin/pulse-agent-freebsd-arm64
|
|
)
|
|
for arch in "${tar_arches[@]}"; do
|
|
tarball="pulse-v${PULSE_VERSION}-${arch}.tar.gz"
|
|
|
|
# Check binaries (note: tarballs use ./ prefix)
|
|
if ! tar -tzf "$tarball" ./bin/pulse >/dev/null 2>&1; then
|
|
error "$(basename $tarball) missing binaries"
|
|
exit 1
|
|
fi
|
|
|
|
check_tar_entries_nonempty "$tarball" "${unified_agent_entries[@]}"
|
|
|
|
# Check scripts
|
|
tar -tzf "$tarball" ./scripts/install-container-agent.sh ./scripts/install-docker.sh ./scripts/install.sh >/dev/null 2>&1 || { error "$(basename $tarball) missing scripts"; exit 1; }
|
|
|
|
# Check VERSION file
|
|
tar -tzf "$tarball" ./VERSION >/dev/null 2>&1 || { error "$(basename $tarball) missing VERSION file"; exit 1; }
|
|
done
|
|
success "Platform-specific tarballs contain all required files (including cross-platform unified agents)"
|
|
|
|
# Validate universal tarball
|
|
section "Validating universal tarball"
|
|
tar -tzf "pulse-v${PULSE_VERSION}.tar.gz" ./VERSION >/dev/null 2>&1 || { error "Universal tarball missing VERSION file"; exit 1; }
|
|
|
|
# Validate universal tarball contains all agent binaries for download endpoint
|
|
info "Validating universal tarball contains all agent binaries..."
|
|
check_tar_entries_nonempty "pulse-v${PULSE_VERSION}.tar.gz" "${unified_agent_entries[@]}"
|
|
success "Universal tarball validated (includes cross-platform unified agents)"
|
|
|
|
# Validate macOS tarballs
|
|
tar -tzf "pulse-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz" pulse-agent-darwin-arm64 >/dev/null 2>&1 || { error "macOS unified-agent tarball validation failed"; exit 1; }
|
|
success "macOS agent tarballs validated"
|
|
|
|
# Validate checksums.txt
|
|
info "Validating checksums..."
|
|
sha256sum -c checksums.txt >/dev/null 2>&1 || { error "checksums.txt validation failed"; exit 1; }
|
|
success "checksums.txt validated"
|
|
|
|
release_sbom="pulse-${PULSE_TAG}-release.sbom.spdx.json"
|
|
if ! grep -F " ${release_sbom}" checksums.txt >/dev/null 2>&1; then
|
|
error "checksums.txt is missing ${release_sbom}"
|
|
exit 1
|
|
fi
|
|
success "Release SBOM is listed in checksums.txt"
|
|
|
|
# Validate release signature sidecars
|
|
info "Validating SSH signature sidecars..."
|
|
if [ ! -s "checksums.txt.sshsig" ]; then
|
|
error "Missing or empty checksums.txt.sshsig"
|
|
exit 1
|
|
fi
|
|
|
|
while IFS= read -r line; do
|
|
checksum=$(echo "$line" | awk '{print $1}')
|
|
filename=$(echo "$line" | awk '{print $2}')
|
|
|
|
[ -n "$checksum" ] || continue
|
|
[ -n "$filename" ] || continue
|
|
|
|
if [ ! -s "${filename}.sshsig" ]; then
|
|
error "Missing or empty ${filename}.sshsig"
|
|
exit 1
|
|
fi
|
|
done < checksums.txt
|
|
success "SSH signature sidecars validated"
|
|
|
|
# Actually run the README's documented verification step against install.sh.sshsig.
|
|
# The README ships a hardcoded ed25519 pubkey and tells customers to verify
|
|
# install.sh with it before running. Across v6 rc.2 → rc.5 (~20 days) the README
|
|
# pinned a stale key (Ds21c5...) that didn't match the actual pipeline signing
|
|
# key (MZd/...), so any customer who followed the secure-install path got
|
|
# "Could not verify signature" and aborted. This check extracts the README's
|
|
# pinned key and runs the exact verification command, so any drift between
|
|
# documented key and actual signing key fails the release.
|
|
info "Validating README pinned signature key matches install.sh.sshsig..."
|
|
readme_path="$(cd "$(dirname "$0")/.." && pwd)/README.md"
|
|
if [ ! -f "$readme_path" ]; then
|
|
error "README.md not found at $readme_path — cannot validate documented signature key"
|
|
exit 1
|
|
fi
|
|
readme_signing_key=$(grep -oE "ssh-ed25519 [A-Za-z0-9+/=]+ pulse-installer" "$readme_path" | head -1)
|
|
if [ -z "$readme_signing_key" ]; then
|
|
error "Could not extract ed25519 pulse-installer key from README.md secure-install snippet"
|
|
exit 1
|
|
fi
|
|
if ! command -v ssh-keygen >/dev/null 2>&1; then
|
|
error "ssh-keygen not found — required to validate the README-documented signature path"
|
|
exit 1
|
|
fi
|
|
readme_allowed_signers=$(mktemp)
|
|
printf 'pulse-installer %s\n' "$readme_signing_key" > "$readme_allowed_signers"
|
|
if ! ssh-keygen -Y verify \
|
|
-f "$readme_allowed_signers" \
|
|
-I pulse-installer \
|
|
-n pulse-install \
|
|
-s install.sh.sshsig < install.sh >/dev/null 2>&1; then
|
|
rm -f "$readme_allowed_signers"
|
|
error "README's pinned signature key does not verify install.sh.sshsig"
|
|
error "Customers who follow the README's secure-install ssh-keygen step will see 'Could not verify signature' and abort"
|
|
error "Either update README.md/docs/INSTALL.md with the correct pulse-installer pubkey, or fix the release signing key"
|
|
exit 1
|
|
fi
|
|
rm -f "$readme_allowed_signers"
|
|
success "README pinned signature key verifies install.sh.sshsig"
|
|
|
|
# Validate individual .sha256 files exist and match checksums.txt
|
|
info "Validating individual .sha256 files..."
|
|
while IFS= read -r line; do
|
|
checksum=$(echo "$line" | awk '{print $1}')
|
|
filename=$(echo "$line" | awk '{print $2}')
|
|
|
|
[ -n "$checksum" ] || continue
|
|
[ -n "$filename" ] || continue
|
|
|
|
# Check .sha256 file exists
|
|
if [ ! -f "${filename}.sha256" ]; then
|
|
error "Missing ${filename}.sha256"
|
|
exit 1
|
|
fi
|
|
|
|
# Check .sha256 file content matches checksums.txt
|
|
sha256_content=$(cat "${filename}.sha256")
|
|
expected_content="${checksum} ${filename}"
|
|
if [ "$sha256_content" != "$expected_content" ]; then
|
|
error "${filename}.sha256 content mismatch"
|
|
exit 1
|
|
fi
|
|
done < checksums.txt
|
|
success "Individual .sha256 files validated"
|
|
|
|
popd >/dev/null
|
|
|
|
echo ""
|
|
|
|
#=============================================================================
|
|
# VERSION EMBEDDING VALIDATION (EXTRACTED TARBALL)
|
|
#=============================================================================
|
|
info "=== Version Embedding Validation (Extracted Binaries) ==="
|
|
|
|
# Extract linux-amd64 tarball for testing
|
|
extract_dir="$tmp_root/linux-amd64"
|
|
mkdir -p "$extract_dir"
|
|
tar -xzf "$RELEASE_DIR/pulse-v${PULSE_VERSION}-linux-amd64.tar.gz" -C "$extract_dir"
|
|
|
|
info "Testing extracted binaries from linux-amd64 tarball..."
|
|
|
|
# Test Pulse server
|
|
"$extract_dir/bin/pulse" version 2>/dev/null | grep -Fx "Pulse $PULSE_TAG" >/dev/null || { error "Extracted pulse binary version mismatch"; exit 1; }
|
|
success "Extracted pulse binary: $PULSE_TAG"
|
|
|
|
# Ensure extracted pulse binary contains embedded version string metadata.
|
|
grep -aF "$PULSE_TAG" "$extract_dir/bin/pulse" >/dev/null || { error "Extracted pulse binary does not contain expected version string"; exit 1; }
|
|
success "Extracted pulse binary contains expected version string: $PULSE_TAG"
|
|
|
|
# Test VERSION file
|
|
grep -Fx "$PULSE_VERSION" "$extract_dir/VERSION" >/dev/null || { error "Extracted VERSION file mismatch"; exit 1; }
|
|
success "Extracted VERSION file: $PULSE_VERSION"
|
|
|
|
echo ""
|
|
|
|
# NOTE: Standalone binary validation removed - they are NOT in GitHub releases
|
|
# They are only included in Docker images for /download/ endpoints
|
|
|
|
echo ""
|
|
|
|
#=============================================================================
|
|
# OPTIONAL: HELM CHART VALIDATION
|
|
#=============================================================================
|
|
if [ -f "$RELEASE_DIR/pulse-${PULSE_VERSION}.tgz" ]; then
|
|
info "=== Helm Chart Validation ==="
|
|
|
|
if command -v helm >/dev/null 2>&1; then
|
|
# Extract and validate Helm chart
|
|
helm_extract="$tmp_root/helm"
|
|
mkdir -p "$helm_extract"
|
|
tar -xzf "$RELEASE_DIR/pulse-${PULSE_VERSION}.tgz" -C "$helm_extract"
|
|
|
|
# Validate Chart.yaml
|
|
if [ -f "$helm_extract/pulse/Chart.yaml" ]; then
|
|
chart_version=$(grep '^version:' "$helm_extract/pulse/Chart.yaml" | awk '{print $2}')
|
|
app_version=$(grep '^appVersion:' "$helm_extract/pulse/Chart.yaml" | awk '{print $2}' | tr -d '"')
|
|
|
|
if [ "$chart_version" = "$PULSE_VERSION" ]; then
|
|
success "Helm chart version: $chart_version"
|
|
else
|
|
error "Helm chart version mismatch: expected=$PULSE_VERSION actual=$chart_version"
|
|
fi
|
|
|
|
if [ "$app_version" = "$PULSE_VERSION" ]; then
|
|
success "Helm appVersion: $app_version"
|
|
else
|
|
error "Helm appVersion mismatch: expected=$PULSE_VERSION actual=$app_version"
|
|
fi
|
|
else
|
|
warn "Helm Chart.yaml not found in extracted chart"
|
|
fi
|
|
else
|
|
warn "Helm not installed, skipping chart validation"
|
|
fi
|
|
else
|
|
info "Helm chart not found (pulse-${PULSE_VERSION}.tgz) - skipping Helm validation"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ ✓ RELEASE VALIDATION PASSED FOR ${PULSE_TAG} ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ All required artifacts, scripts, binaries, and version ║${NC}"
|
|
echo -e "${GREEN}║ strings validated successfully. ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|