Files
Pulse/scripts/lib/systemd.sh
rcourtman 0fcfad3dc5 feat: add shared script library system and refactor docker-agent installer
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.
2025-10-20 15:13:38 +00:00

189 lines
4.7 KiB
Bash

#!/usr/bin/env bash
#
# Systemd management helpers for Pulse shell scripts.
set -euo pipefail
# Ensure the common library is available 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 SYSTEMD_TIMEOUT_SEC="${SYSTEMD_TIMEOUT_SEC:-20}"
# systemd::safe_systemctl args...
# Executes systemctl with sensible defaults and optional timeout guards.
systemd::safe_systemctl() {
if ! command -v systemctl >/dev/null 2>&1; then
common::fail "systemctl not available on this host"
fi
local -a cmd
systemd::__build_cmd cmd "$@"
systemd::__wrap_timeout cmd
local label="systemctl $*"
common::run --label "${label}" -- "${cmd[@]}"
}
# systemd::detect_service_name [service...]
# Returns the first existing service name from the provided list.
systemd::detect_service_name() {
local -a candidates=("$@")
if ((${#candidates[@]} == 0)); then
candidates=(pulse.service pulse-backend.service pulse-docker-agent.service pulse-hot-dev.service)
fi
local name
for name in "${candidates[@]}"; do
if systemd::service_exists "${name}"; then
printf '%s\n' "$(systemd::__normalize_unit "${name}")"
return 0
fi
done
return 1
}
# systemd::service_exists service
# Returns success if the given service unit exists.
systemd::service_exists() {
local unit
unit="$(systemd::__normalize_unit "${1:-}")" || return 1
if command -v systemctl >/dev/null 2>&1 && ! common::is_dry_run; then
local -a cmd
systemd::__build_cmd cmd "list-unit-files" "${unit}"
systemd::__wrap_timeout cmd
local output=""
if output="$("${cmd[@]}" 2>/dev/null)"; then
[[ "${output}" =~ ^${unit}[[:space:]] ]] && return 0
fi
fi
local paths=(
"/etc/systemd/system/${unit}"
"/lib/systemd/system/${unit}"
"/usr/lib/systemd/system/${unit}"
)
local path
for path in "${paths[@]}"; do
if [[ -f "${path}" ]]; then
return 0
fi
done
return 1
}
# systemd::is_active service
# Checks whether the given service is active.
systemd::is_active() {
local unit
unit="$(systemd::__normalize_unit "${1:-}")" || return 1
if common::is_dry_run; then
return 1
fi
if ! command -v systemctl >/dev/null 2>&1; then
return 1
fi
local -a cmd
systemd::__build_cmd cmd "is-active" "--quiet" "${unit}"
systemd::__wrap_timeout cmd
if "${cmd[@]}" >/dev/null 2>&1; then
return 0
fi
return 1
}
# systemd::create_service /path/to/unit.service
# Reads unit file content from stdin and writes it to the supplied path.
systemd::create_service() {
local target="${1:-}"
local mode="${2:-0644}"
if [[ -z "${target}" ]]; then
common::fail "systemd::create_service requires a target path"
fi
local content
content="$(cat)"
if common::is_dry_run; then
common::log_info "[dry-run] Would write systemd unit ${target}"
common::log_debug "${content}"
return 0
fi
mkdir -p "$(dirname "${target}")"
printf '%s' "${content}" > "${target}"
chmod "${mode}" "${target}"
common::log_info "Wrote unit file: ${target}"
}
# systemd::enable_and_start service
# Reloads systemd, enables, and starts the given service.
systemd::enable_and_start() {
local unit
unit="$(systemd::__normalize_unit "${1:-}")" || common::fail "Invalid systemd unit name"
systemd::safe_systemctl daemon-reload
systemd::safe_systemctl enable "${unit}"
systemd::safe_systemctl start "${unit}"
}
# systemd::restart service
# Safely restarts the given service.
systemd::restart() {
local unit
unit="$(systemd::__normalize_unit "${1:-}")" || common::fail "Invalid systemd unit name"
systemd::safe_systemctl restart "${unit}"
}
# Internal: build systemctl command array.
systemd::__build_cmd() {
local -n ref=$1
shift
ref=("systemctl" "--no-ask-password" "--no-pager")
ref+=("$@")
}
# Internal: wrap command with timeout if necessary.
systemd::__wrap_timeout() {
local -n ref=$1
if ! command -v timeout >/dev/null 2>&1; then
return
fi
if systemd::__should_timeout; then
local -a wrapped=("timeout" "${SYSTEMD_TIMEOUT_SEC}s")
wrapped+=("${ref[@]}")
ref=("${wrapped[@]}")
fi
}
# Internal: determine if we are in a container environment.
systemd::__should_timeout() {
if [[ -f /run/systemd/container ]]; then
return 0
fi
if command -v systemd-detect-virt >/dev/null 2>&1; then
if systemd-detect-virt --quiet --container; then
return 0
fi
fi
return 1
}
# Internal: normalize service names to include .service suffix.
systemd::__normalize_unit() {
local unit="${1:-}"
if [[ -z "${unit}" ]]; then
return 1
fi
if [[ "${unit}" != *.service ]]; then
unit="${unit}.service"
fi
printf '%s\n' "${unit}"
}