Harden release validation for host agent downloads (related to #735)

This commit is contained in:
rcourtman 2025-11-21 10:47:53 +00:00
parent 9a33dc568e
commit 28c0d3d39c
3 changed files with 445 additions and 102 deletions

View file

@ -2438,7 +2438,7 @@ download_pulse() {
print_warn "Host agent binary not found in archive; skipping installation"
fi
install_additional_agent_binaries "$LATEST_RELEASE"
install_additional_agent_binaries "$LATEST_RELEASE" "$TEMP_EXTRACT"
# Install all agent scripts
deploy_agent_scripts "$TEMP_EXTRACT"
@ -2487,7 +2487,7 @@ download_pulse() {
ln -sf "$INSTALL_DIR/bin/pulse-docker-agent" /usr/local/bin/pulse-docker-agent
fi
install_additional_agent_binaries "$LATEST_RELEASE"
install_additional_agent_binaries "$LATEST_RELEASE" "$TEMP_EXTRACT2"
deploy_agent_scripts "$TEMP_EXTRACT2"
@ -2512,26 +2512,74 @@ download_pulse() {
fi # End of SKIP_DOWNLOAD check
}
copy_host_agent_binaries_from_dir() {
local source_dir="$1"
if [[ -z "$source_dir" ]] || [[ ! -d "$source_dir/bin" ]]; then
return 1
fi
local copied=0
shopt -s nullglob
for agent_file in "$source_dir"/bin/pulse-host-agent-*; do
[[ -e "$agent_file" ]] || continue
local base
base=$(basename "$agent_file")
if [[ "$base" == "pulse-host-agent" ]]; then
continue
fi
cp -a "$agent_file" "$INSTALL_DIR/bin/$base"
if [[ ! -L "$INSTALL_DIR/bin/$base" ]]; then
chmod +x "$INSTALL_DIR/bin/$base"
fi
chown -h pulse:pulse "$INSTALL_DIR/bin/$base" || true
copied=1
done
shopt -u nullglob
if [[ $copied -eq 0 ]]; then
return 1
fi
return 0
}
install_additional_agent_binaries() {
local version="$1"
local source_dir="${2:-}"
if [[ -z "$version" ]]; then
return
fi
local targets=("linux-amd64" "linux-arm64" "linux-armv7")
local docker_missing=0
local host_missing=0
for target in "${targets[@]}"; do
local docker_targets=("linux-amd64" "linux-arm64" "linux-armv7")
local host_targets=("linux-amd64" "linux-arm64" "linux-armv7" "linux-armv6" "linux-386" "darwin-amd64" "darwin-arm64" "windows-amd64" "windows-arm64" "windows-386")
# Prefer locally available host agents from the extracted archive to avoid network reliance
copy_host_agent_binaries_from_dir "$source_dir" || true
local docker_missing_targets=()
for target in "${docker_targets[@]}"; do
if [[ ! -f "$INSTALL_DIR/bin/pulse-docker-agent-$target" ]]; then
docker_missing=1
fi
if [[ ! -f "$INSTALL_DIR/bin/pulse-host-agent-$target" ]]; then
host_missing=1
docker_missing_targets+=("$target")
fi
done
if [[ $docker_missing -eq 0 ]] && [[ $host_missing -eq 0 ]]; then
local host_missing_targets=()
for target in "${host_targets[@]}"; do
if [[ "$target" == windows-* ]]; then
if [[ ! -e "$INSTALL_DIR/bin/pulse-host-agent-$target" && ! -e "$INSTALL_DIR/bin/pulse-host-agent-$target.exe" ]]; then
host_missing_targets+=("$target")
fi
else
if [[ ! -e "$INSTALL_DIR/bin/pulse-host-agent-$target" ]]; then
host_missing_targets+=("$target")
fi
fi
done
if [[ ${#docker_missing_targets[@]} -eq 0 ]] && [[ ${#host_missing_targets[@]} -eq 0 ]]; then
return
fi
@ -2582,20 +2630,10 @@ install_additional_agent_binaries() {
fi
done
# Install host agent binaries
for agent_file in "$temp_dir"/bin/pulse-host-agent-*; do
if [[ -f "$agent_file" ]]; then
local base
base=$(basename "$agent_file")
# Don't copy the wrapper script, only platform-specific binaries
if [[ "$base" == pulse-host-agent-linux-* ]] || [[ "$base" == pulse-host-agent-darwin-* ]] || [[ "$base" == pulse-host-agent-windows-* ]]; then
cp -f "$agent_file" "$INSTALL_DIR/bin/$base"
chmod +x "$INSTALL_DIR/bin/$base"
chown pulse:pulse "$INSTALL_DIR/bin/$base"
host_installed=1
fi
fi
done
# Install host agent binaries (preserve symlinks for Windows targets)
if copy_host_agent_binaries_from_dir "$temp_dir"; then
host_installed=1
fi
if [[ $docker_installed -eq 1 ]]; then
print_success "Additional Docker agent binaries installed"

View file

@ -34,7 +34,37 @@ rm -rf internal/api/frontend-modern
mkdir -p internal/api/frontend-modern
cp -r frontend-modern/dist internal/api/frontend-modern/
# Build for different architectures
# Build host agents for every supported platform/architecture so download endpoints work offline
echo "Building host agents for all platforms..."
declare -A host_agent_builds=(
["linux-amd64"]="GOOS=linux GOARCH=amd64"
["linux-arm64"]="GOOS=linux GOARCH=arm64"
["linux-armv7"]="GOOS=linux GOARCH=arm GOARM=7"
["linux-armv6"]="GOOS=linux GOARCH=arm GOARM=6"
["linux-386"]="GOOS=linux GOARCH=386"
["darwin-amd64"]="GOOS=darwin GOARCH=amd64"
["darwin-arm64"]="GOOS=darwin GOARCH=arm64"
["windows-amd64"]="GOOS=windows GOARCH=amd64"
["windows-arm64"]="GOOS=windows GOARCH=arm64"
["windows-386"]="GOOS=windows GOARCH=386"
)
host_agent_order=(linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386 darwin-amd64 darwin-arm64 windows-amd64 windows-arm64 windows-386)
for target in "${host_agent_order[@]}"; do
build_env="${host_agent_builds[$target]}"
output_path="$BUILD_DIR/pulse-host-agent-$target"
if [[ "$target" == windows-* ]]; then
output_path="${output_path}.exe"
fi
env $build_env go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$output_path" \
./cmd/pulse-host-agent
done
# Build for different architectures (server + docker agent + sensor proxy)
declare -A builds=(
["linux-amd64"]="GOOS=linux GOARCH=amd64"
["linux-arm64"]="GOOS=linux GOARCH=arm64"
@ -42,11 +72,11 @@ declare -A builds=(
["linux-armv6"]="GOOS=linux GOARCH=arm GOARM=6"
["linux-386"]="GOOS=linux GOARCH=386"
)
build_order=(linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386)
for build_name in "${!builds[@]}"; do
for build_name in "${build_order[@]}"; do
echo "Building for $build_name..."
# Get build environment
build_env="${builds[$build_name]}"
build_time=$(date -u '+%Y-%m-%d_%H:%M:%S')
@ -66,34 +96,43 @@ for build_name in "${!builds[@]}"; do
-o "$BUILD_DIR/pulse-docker-agent-$build_name" \
./cmd/pulse-docker-agent
# Build host agent binary
env $build_env go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-$build_name" \
./cmd/pulse-host-agent
# Build temperature proxy binary
env $build_env go build \
-ldflags="-s -w -X main.Version=v${VERSION} -X main.BuildTime=${build_time} -X main.GitCommit=${git_commit}" \
-trimpath \
-o "$BUILD_DIR/pulse-sensor-proxy-$build_name" \
./cmd/pulse-sensor-proxy
# Create release archive with proper structure
done
# Create platform-specific tarballs that include all host agent binaries for download endpoints
for build_name in "${build_order[@]}"; do
echo "Packaging release for $build_name..."
tar_name="pulse-v${VERSION}-${build_name}.tar.gz"
# Create staging directory
staging_dir="$BUILD_DIR/staging-$build_name"
rm -rf "$staging_dir"
mkdir -p "$staging_dir/bin"
mkdir -p "$staging_dir/scripts"
# Copy binaries and VERSION file
# Copy architecture-specific runtime binaries
cp "$BUILD_DIR/pulse-$build_name" "$staging_dir/bin/pulse"
cp "$BUILD_DIR/pulse-docker-agent-$build_name" "$staging_dir/bin/pulse-docker-agent"
cp "$BUILD_DIR/pulse-host-agent-$build_name" "$staging_dir/bin/pulse-host-agent"
cp "$BUILD_DIR/pulse-sensor-proxy-$build_name" "$staging_dir/bin/pulse-sensor-proxy"
# Copy host agent binaries for every supported platform/architecture
for target in "${host_agent_order[@]}"; do
src="$BUILD_DIR/pulse-host-agent-$target"
dest="$staging_dir/bin/pulse-host-agent-$target"
if [[ "$target" == windows-* ]]; then
src="${src}.exe"
dest="${dest}.exe"
fi
cp "$src" "$dest"
done
( cd "$staging_dir/bin" && ln -sf pulse-host-agent-windows-amd64.exe pulse-host-agent-windows-amd64 && ln -sf pulse-host-agent-windows-arm64.exe pulse-host-agent-windows-arm64 && ln -sf pulse-host-agent-windows-386.exe pulse-host-agent-windows-386 )
# Copy scripts and VERSION metadata
cp "scripts/install-docker-agent.sh" "$staging_dir/scripts/install-docker-agent.sh"
cp "scripts/install-container-agent.sh" "$staging_dir/scripts/install-container-agent.sh"
cp "scripts/install-host-agent.sh" "$staging_dir/scripts/install-host-agent.sh"
@ -104,15 +143,13 @@ for build_name in "${!builds[@]}"; do
cp "scripts/install-docker.sh" "$staging_dir/scripts/install-docker.sh"
chmod 755 "$staging_dir/scripts/"*.sh "$staging_dir/scripts/"*.ps1
echo "$VERSION" > "$staging_dir/VERSION"
# Create tarball from staging directory
cd "$staging_dir"
tar -czf "../../$RELEASE_DIR/$tar_name" .
cd ../..
# Cleanup staging
rm -rf "$staging_dir"
echo "Created $RELEASE_DIR/$tar_name"
done
@ -124,7 +161,7 @@ mkdir -p "$universal_dir/bin"
mkdir -p "$universal_dir/scripts"
# Copy all binaries to bin/ directory to maintain consistent structure
for build_name in "${!builds[@]}"; do
for build_name in "${build_order[@]}"; do
cp "$BUILD_DIR/pulse-$build_name" "$universal_dir/bin/pulse-${build_name}"
cp "$BUILD_DIR/pulse-docker-agent-$build_name" "$universal_dir/bin/pulse-docker-agent-${build_name}"
cp "$BUILD_DIR/pulse-host-agent-$build_name" "$universal_dir/bin/pulse-host-agent-${build_name}"
@ -237,43 +274,6 @@ chmod +x "$universal_dir/bin/pulse-host-agent"
# Add VERSION file
echo "$VERSION" > "$universal_dir/VERSION"
# Build host agent for macOS
echo "Building host agent for macOS amd64..."
env GOOS=darwin GOARCH=amd64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-darwin-amd64" \
./cmd/pulse-host-agent
echo "Building host agent for macOS arm64..."
env GOOS=darwin GOARCH=arm64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-darwin-arm64" \
./cmd/pulse-host-agent
# Build host agent for Windows
echo "Building host agent for Windows amd64..."
env GOOS=windows GOARCH=amd64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-windows-amd64.exe" \
./cmd/pulse-host-agent
echo "Building host agent for Windows arm64..."
env GOOS=windows GOARCH=arm64 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-windows-arm64.exe" \
./cmd/pulse-host-agent
echo "Building host agent for Windows 386..."
env GOOS=windows GOARCH=386 go build \
-ldflags="-s -w -X github.com/rcourtman/pulse-go-rewrite/internal/hostagent.Version=v${VERSION}" \
-trimpath \
-o "$BUILD_DIR/pulse-host-agent-windows-386.exe" \
./cmd/pulse-host-agent
# Package standalone host agent binaries
tar -czf "$RELEASE_DIR/pulse-host-agent-v${VERSION}-darwin-amd64.tar.gz" -C "$BUILD_DIR" pulse-host-agent-darwin-amd64
tar -czf "$RELEASE_DIR/pulse-host-agent-v${VERSION}-darwin-arm64.tar.gz" -C "$BUILD_DIR" pulse-host-agent-darwin-arm64
@ -358,3 +358,76 @@ echo
echo "Release build complete!"
echo "Archives created in $RELEASE_DIR/"
ls -lh $RELEASE_DIR/
# Create host-agent manifest (per tarball) for validation/debugging
manifest_path="$RELEASE_DIR/host-agent-manifest.json"
echo "Generating host-agent manifest at $manifest_path..."
python3 - <<'EOF' "$RELEASE_DIR" "$VERSION" "$manifest_path"
import json
import os
import sys
import tarfile
release_dir = sys.argv[1]
version = sys.argv[2]
manifest_path = sys.argv[3]
tar_arches = [
"linux-amd64",
"linux-arm64",
"linux-armv7",
"linux-armv6",
"linux-386",
]
host_agents = [
"pulse-host-agent-linux-amd64",
"pulse-host-agent-linux-arm64",
"pulse-host-agent-linux-armv7",
"pulse-host-agent-linux-armv6",
"pulse-host-agent-linux-386",
"pulse-host-agent-darwin-amd64",
"pulse-host-agent-darwin-arm64",
"pulse-host-agent-windows-amd64.exe",
"pulse-host-agent-windows-arm64.exe",
"pulse-host-agent-windows-386.exe",
"pulse-host-agent-windows-amd64",
"pulse-host-agent-windows-arm64",
"pulse-host-agent-windows-386",
]
manifest = {
"version": version,
"tarballs": {},
"universal": [],
}
def collect_agents(tar_path):
found = []
try:
with tarfile.open(tar_path, "r:gz") as tf:
names = set(m.name for m in tf.getmembers() if (m.isfile() or m.issym()))
for agent in host_agents:
target = f"./bin/{agent}"
if target in names:
found.append(agent)
except Exception as exc:
print(f"Failed to read {tar_path}: {exc}", file=sys.stderr)
return sorted(found)
# Platform tarballs
for arch in tar_arches:
tarball = os.path.join(release_dir, f"pulse-v{version}-{arch}.tar.gz")
if os.path.exists(tarball):
manifest["tarballs"][arch] = collect_agents(tarball)
# Universal tarball
universal_tar = os.path.join(release_dir, f"pulse-v{version}.tar.gz")
if os.path.exists(universal_tar):
manifest["universal"] = collect_agents(universal_tar)
with open(manifest_path, "w", encoding="utf-8") as handle:
json.dump(manifest, handle, indent=2)
print(json.dumps(manifest, indent=2))
EOF

View file

@ -21,6 +21,11 @@ error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}
section() {
echo ""
echo -e "${BLUE}=== ${1} ===${NC}"
}
success() {
echo -e "${GREEN}[✓]${NC} $*"
}
@ -33,6 +38,37 @@ 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
}
if [ $# -lt 1 ]; then
error "Usage: $0 <pulse-version> [image] [release-dir] [--skip-docker]"
exit 1
@ -73,7 +109,8 @@ fi
# Create temp directory for extractions
tmp_root=$(mktemp -d)
trap 'rm -rf "$tmp_root"' EXIT
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"
@ -88,7 +125,7 @@ if [ "$SKIP_DOCKER" = false ]; then
# 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; actual=$(cat /VERSION | tr -d "\r\n"); [ "$actual" = "$EXPECTED_VERSION" ] || { echo "VERSION mismatch: expected=$EXPECTED_VERSION actual=$actual" >&2; exit 1; }' || { error "VERSION file mismatch in Docker image"; exit 1; }
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
@ -120,6 +157,124 @@ if [ "$SKIP_DOCKER" = false ]; then
docker run --rm --entrypoint /bin/sh -e EXPECTED_TAG="$PULSE_TAG" "$IMAGE" -c 'set -euo pipefail; grep -aF "$EXPECTED_TAG" /opt/pulse/bin/pulse-docker-agent-linux-amd64 >/dev/null' || { error "Docker agent version string not found"; exit 1; }
success "Docker agent version embedded: $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 entry in "${download_matrix[@]}"; do
set -- $entry
platform=$1
arch=$2
url="http://127.0.0.1:${HOST_PORT}/download/pulse-host-agent?platform=${platform}&arch=${arch}"
tmp_file=$(mktemp)
if ! curl -fsS -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
rm -f "$tmp_file"
done
success "Download endpoints returned binaries for all platforms/architectures"
checksum_url="http://127.0.0.1:${HOST_PORT}/download/pulse-host-agent.sha256?platform=linux&arch=amd64"
checksum_tmp=$(mktemp)
if curl -fsS -o "$checksum_tmp" "$checksum_url"; then
if ! grep -Eq '^[0-9a-f]{64}$' "$checksum_tmp"; then
error "Invalid checksum response from $checksum_url"
exit 1
fi
success "Checksum endpoint responded with SHA256"
else
warn "Checksum endpoint unavailable (non-blocking): $checksum_url"
fi
rm -f "$checksum_tmp"
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)
if ! docker exec "$SMOKE_CONTAINER" wget -qO- "http://127.0.0.1:7655/download/pulse-host-agent?platform=linux&arch=amd64" > "$offline_tmp"; 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
rm -f "$offline_tmp"
success "Offline self-heal: download endpoint works 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) ==="
@ -141,6 +296,7 @@ info "Checking required release assets..."
required_assets=(
"install.sh"
"checksums.txt"
"host-agent-manifest.json"
"pulse-v${PULSE_VERSION}.tar.gz"
"pulse-v${PULSE_VERSION}-linux-amd64.tar.gz"
"pulse-v${PULSE_VERSION}-linux-arm64.tar.gz"
@ -168,9 +324,92 @@ if [ $missing_count -gt 0 ]; then
fi
success "All ${#required_assets[@]} required release assets present"
# Validate host-agent manifest matches expected set
section "Validating host-agent manifest"
host_agent_manifest="host-agent-manifest.json"
python3 - "$host_agent_manifest" "$PULSE_VERSION" <<'EOF' || { error "Host-agent manifest validation failed"; exit 1; }
import json
import sys
import os
manifest_path = sys.argv[1]
version = sys.argv[2]
with open(manifest_path, "r", encoding="utf-8") as handle:
manifest = json.load(handle)
expected_agents = {
"pulse-host-agent-linux-amd64",
"pulse-host-agent-linux-arm64",
"pulse-host-agent-linux-armv7",
"pulse-host-agent-linux-armv6",
"pulse-host-agent-linux-386",
"pulse-host-agent-darwin-amd64",
"pulse-host-agent-darwin-arm64",
"pulse-host-agent-windows-amd64.exe",
"pulse-host-agent-windows-arm64.exe",
"pulse-host-agent-windows-386.exe",
"pulse-host-agent-windows-amd64",
"pulse-host-agent-windows-arm64",
"pulse-host-agent-windows-386",
}
def check_set(name, found):
missing = expected_agents - set(found)
extra = set(found) - expected_agents
if missing or extra:
msg = []
if missing:
msg.append(f"{name} missing: {sorted(missing)}")
if extra:
msg.append(f"{name} unexpected: {sorted(extra)}")
print(" ; ".join(msg))
return False
return True
ok = True
if manifest.get("version") != version:
print(f"Manifest version mismatch: expected {version}, got {manifest.get('version')}")
ok = False
universal = manifest.get("universal", [])
if not check_set("universal", universal):
ok = False
for arch in ["linux-amd64","linux-arm64","linux-armv7","linux-armv6","linux-386"]:
found = manifest.get("tarballs", {}).get(arch)
if found is None:
print(f"Missing tarball entry in manifest for {arch}")
ok = False
continue
if not check_set(arch, found):
ok = False
if not ok:
sys.exit(1)
EOF
success "Host-agent manifest matches expected platform/arch matrix"
# Validate tarball contents
info "Validating tarball contents..."
for arch in linux-amd64 linux-arm64 linux-armv7; do
section "Validating tarball contents"
tar_arches=(linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-386)
host_agent_entries=(
./bin/pulse-host-agent-linux-amd64
./bin/pulse-host-agent-linux-arm64
./bin/pulse-host-agent-linux-armv7
./bin/pulse-host-agent-linux-armv6
./bin/pulse-host-agent-linux-386
./bin/pulse-host-agent-darwin-amd64
./bin/pulse-host-agent-darwin-arm64
./bin/pulse-host-agent-windows-amd64.exe
./bin/pulse-host-agent-windows-arm64.exe
./bin/pulse-host-agent-windows-386.exe
./bin/pulse-host-agent-windows-amd64
./bin/pulse-host-agent-windows-arm64
./bin/pulse-host-agent-windows-386
)
for arch in "${tar_arches[@]}"; do
tarball="pulse-v${PULSE_VERSION}-${arch}.tar.gz"
# Check binaries (note: tarballs use ./ prefix)
@ -179,31 +418,24 @@ for arch in linux-amd64 linux-arm64 linux-armv7; do
exit 1
fi
check_tar_entries_nonempty "$tarball" "${host_agent_entries[@]}"
# Check scripts
tar -tzf "$tarball" ./scripts/install-docker-agent.sh ./scripts/install-container-agent.sh ./scripts/install-host-agent.sh ./scripts/install-host-agent.ps1 ./scripts/uninstall-host-agent.sh ./scripts/uninstall-host-agent.ps1 ./scripts/install-sensor-proxy.sh ./scripts/install-docker.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"
success "Platform-specific tarballs contain all required files (including cross-platform host 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 Windows/macOS binaries for download endpoint
info "Validating universal tarball contains Windows/macOS binaries..."
for binary in \
./bin/pulse-host-agent-darwin-amd64 \
./bin/pulse-host-agent-darwin-arm64 \
./bin/pulse-host-agent-windows-amd64.exe \
./bin/pulse-host-agent-windows-arm64.exe \
./bin/pulse-host-agent-windows-386.exe \
./bin/pulse-host-agent-windows-amd64 \
./bin/pulse-host-agent-windows-arm64 \
./bin/pulse-host-agent-windows-386; do
tar -tzf "pulse-v${PULSE_VERSION}.tar.gz" "$binary" >/dev/null 2>&1 || { error "Universal tarball missing $binary"; exit 1; }
done
success "Universal tarball validated (includes Windows/macOS binaries)"
# Validate universal tarball contains all host agent binaries for download endpoint
info "Validating universal tarball contains all host agent binaries..."
check_tar_entries_nonempty "pulse-v${PULSE_VERSION}.tar.gz" "${host_agent_entries[@]}"
success "Universal tarball validated (includes cross-platform host agents)"
# Validate macOS tarball
tar -tzf "pulse-host-agent-v${PULSE_VERSION}-darwin-arm64.tar.gz" pulse-host-agent-darwin-arm64 >/dev/null 2>&1 || { error "macOS tarball validation failed"; exit 1; }