mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Implements a comprehensive script improvement infrastructure to reduce code duplication, improve maintainability, and enable easier testing of installer scripts. ## New Infrastructure ### Shared Library System (scripts/lib/) - common.sh: Core utilities (logging, sudo, dry-run, cleanup management) - systemd.sh: Service management helpers with container-safe systemctl - http.sh: HTTP/download helpers with curl/wget fallback and retry logic - README.md: Complete API documentation for all library functions ### Bundler System - scripts/bundle.sh: Concatenates library modules into single-file installers - scripts/bundle.manifest: Defines bundling configuration for distributables - Enables both modular development and curl|bash distribution ### Test Infrastructure - scripts/tests/run.sh: Test harness for running all smoke tests - scripts/tests/test-common-lib.sh: Common library validation (5 tests) - scripts/tests/test-docker-agent-v2.sh: Installer smoke tests (4 tests) - scripts/tests/integration/: Container-based integration tests (5 scenarios) - All tests passing ✓ ## Refactored Installer ### install-docker-agent-v2.sh - Reduced from 1098 to 563 lines (48% code reduction) - Uses shared libraries for all common operations - NEW: --dry-run flag support - Maintains 100% backward compatibility with original - Fully tested with smoke and integration tests ### Key Improvements - Sudo escalation: 100+ lines → 1 function call - Download logic: 51 lines → 1 function call - Service creation: 33 lines → 2 function calls - Logging: Standardized across all operations - Error handling: Improved with common library ## Documentation ### Rollout Strategy (docs/installer-v2-rollout.md) - 3-phase rollout plan (Alpha → Beta → GA) - Feature flag mechanism for gradual deployment - Testing checklist and success metrics - Rollback procedures and communication plan ### Developer Guides - docs/script-library-guide.md: Complete library usage guide - docs/CONTRIBUTING-SCRIPTS.md: Contribution workflow - docs/installer-v2-quickref.md: Quick reference for operators ## Metrics - Code reduction: 48% (1098 → 563 lines) - Reusable functions: 0 → 30+ - Test coverage: 0 → 8 test scenarios - Documentation: 0 → 5 comprehensive guides ## Testing All tests passing: - Smoke tests: 2/2 passed (8 test cases) - Integration tests: 5/5 scenarios passed - Bundled output: Syntax validated, dry-run tested ## Next Steps This lays the foundation for migrating other installers (install.sh, install-sensor-proxy.sh) to use the same pattern, reducing overall maintenance burden and improving code quality across the project.
285 lines
6.6 KiB
Bash
285 lines
6.6 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# HTTP helpers for Pulse shell scripts (downloads, API calls, GitHub queries).
|
|
|
|
set -euo pipefail
|
|
|
|
# Ensure common library is loaded when sourced directly.
|
|
if ! declare -F common::log_info >/dev/null 2>&1; then
|
|
# shellcheck disable=SC1091
|
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
|
|
fi
|
|
|
|
declare -g HTTP_DEFAULT_RETRIES="${HTTP_DEFAULT_RETRIES:-3}"
|
|
declare -g HTTP_DEFAULT_BACKOFF="${HTTP_DEFAULT_BACKOFF:-1 3 5}"
|
|
|
|
# http::detect_download_tool
|
|
# Emits the preferred download tool (curl/wget) or fails if neither exist.
|
|
http::detect_download_tool() {
|
|
if command -v curl >/dev/null 2>&1; then
|
|
printf 'curl\n'
|
|
return 0
|
|
fi
|
|
if command -v wget >/dev/null 2>&1; then
|
|
printf 'wget\n'
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# http::download --url URL --output PATH [--insecure] [--quiet] [--header "Name: value"]
|
|
# [--retries N] [--backoff "1 3 5"]
|
|
# Downloads the specified URL to PATH using curl or wget.
|
|
http::download() {
|
|
local url=""
|
|
local output=""
|
|
local insecure=false
|
|
local quiet=false
|
|
local retries="${HTTP_DEFAULT_RETRIES}"
|
|
local backoff="${HTTP_DEFAULT_BACKOFF}"
|
|
local -a headers=()
|
|
|
|
while (($#)); do
|
|
case "$1" in
|
|
--url)
|
|
url="$2"
|
|
shift 2
|
|
;;
|
|
--output)
|
|
output="$2"
|
|
shift 2
|
|
;;
|
|
--insecure)
|
|
insecure=true
|
|
shift
|
|
;;
|
|
--quiet)
|
|
quiet=true
|
|
shift
|
|
;;
|
|
--header)
|
|
headers+=("$2")
|
|
shift 2
|
|
;;
|
|
--retries)
|
|
retries="$2"
|
|
shift 2
|
|
;;
|
|
--backoff)
|
|
backoff="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
common::log_warn "Unknown flag for http::download: $1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${url}" || -z "${output}" ]]; then
|
|
common::fail "http::download requires --url and --output"
|
|
fi
|
|
|
|
if common::is_dry_run; then
|
|
common::log_info "[dry-run] Download ${url} -> ${output}"
|
|
return 0
|
|
fi
|
|
|
|
local tool
|
|
if ! tool="$(http::detect_download_tool)"; then
|
|
common::fail "No download tool available (install curl or wget)"
|
|
fi
|
|
|
|
mkdir -p "$(dirname "${output}")"
|
|
|
|
local -a cmd
|
|
if [[ "${tool}" == "curl" ]]; then
|
|
cmd=(curl -fL --connect-timeout 15)
|
|
if [[ "${quiet}" == true ]]; then
|
|
cmd+=(-sS)
|
|
else
|
|
cmd+=(--progress-bar)
|
|
fi
|
|
if [[ "${insecure}" == true ]]; then
|
|
cmd+=(-k)
|
|
fi
|
|
local header
|
|
for header in "${headers[@]}"; do
|
|
cmd+=(-H "${header}")
|
|
done
|
|
cmd+=(-o "${output}" "${url}")
|
|
else
|
|
cmd=(wget --tries=3)
|
|
if [[ "${quiet}" == true ]]; then
|
|
cmd+=(-q)
|
|
else
|
|
cmd+=(--progress=bar:force)
|
|
fi
|
|
if [[ "${insecure}" == true ]]; then
|
|
cmd+=(--no-check-certificate)
|
|
fi
|
|
local header
|
|
for header in "${headers[@]}"; do
|
|
cmd+=(--header="${header}")
|
|
done
|
|
cmd+=(-O "${output}" "${url}")
|
|
fi
|
|
|
|
local -a run_args=(--label "download ${url}")
|
|
[[ -n "${retries}" ]] && run_args+=(--retries "${retries}")
|
|
[[ -n "${backoff}" ]] && run_args+=(--backoff "${backoff}")
|
|
|
|
common::run "${run_args[@]}" -- "${cmd[@]}"
|
|
}
|
|
|
|
# http::api_call --url URL [--method METHOD] [--token TOKEN] [--bearer TOKEN]
|
|
# [--body DATA] [--header "Name: value"] [--insecure]
|
|
# Performs an API request and prints the response body.
|
|
http::api_call() {
|
|
local url=""
|
|
local method="GET"
|
|
local token=""
|
|
local bearer=""
|
|
local body=""
|
|
local insecure=false
|
|
local -a headers=()
|
|
|
|
while (($#)); do
|
|
case "$1" in
|
|
--url)
|
|
url="$2"
|
|
shift 2
|
|
;;
|
|
--method)
|
|
method="$2"
|
|
shift 2
|
|
;;
|
|
--token)
|
|
token="$2"
|
|
shift 2
|
|
;;
|
|
--bearer)
|
|
bearer="$2"
|
|
shift 2
|
|
;;
|
|
--body)
|
|
body="$2"
|
|
shift 2
|
|
;;
|
|
--header)
|
|
headers+=("$2")
|
|
shift 2
|
|
;;
|
|
--insecure)
|
|
insecure=true
|
|
shift
|
|
;;
|
|
*)
|
|
common::log_warn "Unknown flag for http::api_call: $1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${url}" ]]; then
|
|
common::fail "http::api_call requires --url"
|
|
fi
|
|
|
|
if common::is_dry_run; then
|
|
common::log_info "[dry-run] API ${method} ${url}"
|
|
return 0
|
|
fi
|
|
|
|
local tool
|
|
if ! tool="$(http::detect_download_tool)"; then
|
|
common::fail "No HTTP client available (install curl or wget)"
|
|
fi
|
|
|
|
local -a cmd
|
|
if [[ "${tool}" == "curl" ]]; then
|
|
cmd=(curl -fsSL)
|
|
[[ "${insecure}" == true ]] && cmd+=(-k)
|
|
[[ -n "${method}" ]] && cmd+=(-X "${method}")
|
|
if [[ -n "${body}" ]]; then
|
|
cmd+=(-d "${body}")
|
|
fi
|
|
if [[ -n "${token}" ]]; then
|
|
headers+=("X-API-Token: ${token}")
|
|
fi
|
|
if [[ -n "${bearer}" ]]; then
|
|
headers+=("Authorization: Bearer ${bearer}")
|
|
fi
|
|
local header
|
|
for header in "${headers[@]}"; do
|
|
cmd+=(-H "${header}")
|
|
done
|
|
cmd+=("${url}")
|
|
else
|
|
cmd=(wget -qO-)
|
|
[[ "${insecure}" == true ]] && cmd+=(--no-check-certificate)
|
|
[[ -n "${method}" ]] && cmd+=(--method="${method}")
|
|
if [[ -n "${body}" ]]; then
|
|
cmd+=(--body-data="${body}")
|
|
fi
|
|
if [[ -n "${token}" ]]; then
|
|
cmd+=(--header="X-API-Token: ${token}")
|
|
fi
|
|
if [[ -n "${bearer}" ]]; then
|
|
cmd+=(--header="Authorization: Bearer ${bearer}")
|
|
fi
|
|
local header
|
|
for header in "${headers[@]}"; do
|
|
cmd+=(--header="${header}")
|
|
done
|
|
cmd+=("${url}")
|
|
fi
|
|
|
|
common::run_capture --label "api ${method} ${url}" -- "${cmd[@]}"
|
|
}
|
|
|
|
# http::get_github_latest_release owner/repo
|
|
# Echoes the latest release tag for a GitHub repository.
|
|
http::get_github_latest_release() {
|
|
local repo="${1:-}"
|
|
if [[ -z "${repo}" ]]; then
|
|
common::fail "http::get_github_latest_release requires owner/repo argument"
|
|
fi
|
|
|
|
local response
|
|
response="$(http::api_call --url "https://api.github.com/repos/${repo}/releases/latest" --header "Accept: application/vnd.github+json" 2>/dev/null || true)"
|
|
if [[ -z "${response}" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ "${response}" =~ \"tag_name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
|
|
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "${response}" =~ \"name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then
|
|
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
return 0
|
|
fi
|
|
|
|
common::log_warn "Unable to parse GitHub release tag for ${repo}"
|
|
return 1
|
|
}
|
|
|
|
# http::parse_bool value
|
|
# Parses truthy/falsy strings and prints canonical true/false.
|
|
http::parse_bool() {
|
|
local input="${1:-}"
|
|
local lowered="${input,,}"
|
|
case "${lowered}" in
|
|
1|true|yes|y|on)
|
|
printf 'true\n'
|
|
return 0
|
|
;;
|
|
0|false|no|n|off|"")
|
|
printf 'false\n'
|
|
return 0
|
|
;;
|
|
esac
|
|
return 1
|
|
}
|