Post-Tyranny-Tech-Infrastru.../scripts/check-client-versions.sh
Pieter 0c4d536246 feat: Add version tracking and maintenance monitoring (issue #15)
Complete implementation of automatic version tracking and drift detection:

New Scripts:
- scripts/collect-client-versions.sh: Query deployed versions from Docker
  - Connects via Ansible to running servers
  - Extracts versions from container images
  - Updates registry automatically

- scripts/check-client-versions.sh: Compare versions across clients
  - Multiple formats: table (colorized), CSV, JSON
  - Filter by outdated versions
  - Highlights drift with color coding

- scripts/detect-version-drift.sh: Identify version differences
  - Detects clients with outdated versions
  - Threshold-based staleness detection (default 30 days)
  - Actionable recommendations
  - Exit code 1 if drift detected (CI/monitoring friendly)

Updated Scripts:
- scripts/deploy-client.sh: Auto-collect versions after deployment
- scripts/rebuild-client.sh: Auto-collect versions after rebuild

Documentation:
- docs/maintenance-tracking.md: Complete maintenance guide
  - Version management workflows
  - Security update procedures
  - Monitoring integration examples
  - Troubleshooting guide

Features:
 Automatic version collection from deployed servers
 Multi-client version comparison reports
 Version drift detection with recommendations
 Integration with deployment workflows
 Export to CSV/JSON for external tools
 Canary-first update workflow support

Usage Examples:
```bash
# Collect versions
./scripts/collect-client-versions.sh dev

# Compare all clients
./scripts/check-client-versions.sh

# Detect drift
./scripts/detect-version-drift.sh

# Export for monitoring
./scripts/check-client-versions.sh --format=json
```

Closes #15

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-17 20:53:15 +01:00

251 lines
9.1 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Report software versions across all clients
#
# Usage: ./scripts/check-client-versions.sh [options]
#
# Options:
# --format=table Show as colorized table (default)
# --format=csv Export as CSV
# --format=json Export as JSON
# --app=<name> Filter by application (authentik|nextcloud|traefik|ubuntu)
# --outdated Show only clients with outdated versions
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
REGISTRY_FILE="$PROJECT_ROOT/clients/registry.yml"
# Default options
FORMAT="table"
FILTER_APP=""
SHOW_OUTDATED=false
# Parse arguments
for arg in "$@"; do
case $arg in
--format=*)
FORMAT="${arg#*=}"
;;
--app=*)
FILTER_APP="${arg#*=}"
;;
--outdated)
SHOW_OUTDATED=true
;;
*)
echo "Unknown option: $arg"
echo "Usage: $0 [--format=table|csv|json] [--app=<name>] [--outdated]"
exit 1
;;
esac
done
# Check if yq is available
if ! command -v yq &> /dev/null; then
echo -e "${RED}Error: 'yq' not found. Install with: brew install yq${NC}"
exit 1
fi
# Check if registry exists
if [ ! -f "$REGISTRY_FILE" ]; then
echo -e "${RED}Error: Registry file not found: $REGISTRY_FILE${NC}"
exit 1
fi
# Get list of clients
CLIENTS=$(yq eval '.clients | keys | .[]' "$REGISTRY_FILE" 2>/dev/null)
if [ -z "$CLIENTS" ]; then
echo -e "${YELLOW}No clients found in registry${NC}"
exit 0
fi
# Determine latest versions (from canary/dev or most common)
declare -A LATEST_VERSIONS
LATEST_VERSIONS[authentik]=$(yq eval '.clients | to_entries | .[].value.versions.authentik' "$REGISTRY_FILE" | sort -V | tail -1)
LATEST_VERSIONS[nextcloud]=$(yq eval '.clients | to_entries | .[].value.versions.nextcloud' "$REGISTRY_FILE" | sort -V | tail -1)
LATEST_VERSIONS[traefik]=$(yq eval '.clients | to_entries | .[].value.versions.traefik' "$REGISTRY_FILE" | sort -V | tail -1)
LATEST_VERSIONS[ubuntu]=$(yq eval '.clients | to_entries | .[].value.versions.ubuntu' "$REGISTRY_FILE" | sort -V | tail -1)
# Function to check if version is outdated
is_outdated() {
local app=$1
local version=$2
local latest=${LATEST_VERSIONS[$app]}
if [ "$version" != "$latest" ] && [ "$version" != "null" ] && [ "$version" != "unknown" ]; then
return 0
else
return 1
fi
}
case $FORMAT in
table)
echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} CLIENT VERSION REPORT${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════════════════════════════════════${NC}"
echo ""
# Header
printf "${CYAN}%-15s %-15s %-15s %-15s %-15s %-15s${NC}\n" \
"CLIENT" "STATUS" "AUTHENTIK" "NEXTCLOUD" "TRAEFIK" "UBUNTU"
echo -e "${CYAN}$(printf '─%.0s' {1..90})${NC}"
# Rows
for client in $CLIENTS; do
status=$(yq eval ".clients.\"$client\".status" "$REGISTRY_FILE")
authentik=$(yq eval ".clients.\"$client\".versions.authentik" "$REGISTRY_FILE")
nextcloud=$(yq eval ".clients.\"$client\".versions.nextcloud" "$REGISTRY_FILE")
traefik=$(yq eval ".clients.\"$client\".versions.traefik" "$REGISTRY_FILE")
ubuntu=$(yq eval ".clients.\"$client\".versions.ubuntu" "$REGISTRY_FILE")
# Skip if filtering by outdated and not outdated
if [ "$SHOW_OUTDATED" = true ]; then
has_outdated=false
is_outdated "authentik" "$authentik" && has_outdated=true
is_outdated "nextcloud" "$nextcloud" && has_outdated=true
is_outdated "traefik" "$traefik" && has_outdated=true
is_outdated "ubuntu" "$ubuntu" && has_outdated=true
if [ "$has_outdated" = false ]; then
continue
fi
fi
# Colorize versions (red if outdated)
authentik_color=$NC
is_outdated "authentik" "$authentik" && authentik_color=$RED
nextcloud_color=$NC
is_outdated "nextcloud" "$nextcloud" && nextcloud_color=$RED
traefik_color=$NC
is_outdated "traefik" "$traefik" && traefik_color=$RED
ubuntu_color=$NC
is_outdated "ubuntu" "$ubuntu" && ubuntu_color=$RED
# Status color
status_color=$GREEN
[ "$status" != "deployed" ] && status_color=$YELLOW
printf "%-15s ${status_color}%-15s${NC} ${authentik_color}%-15s${NC} ${nextcloud_color}%-15s${NC} ${traefik_color}%-15s${NC} ${ubuntu_color}%-15s${NC}\n" \
"$client" "$status" "$authentik" "$nextcloud" "$traefik" "$ubuntu"
done
echo ""
echo -e "${CYAN}Latest versions:${NC}"
echo " Authentik: ${LATEST_VERSIONS[authentik]}"
echo " Nextcloud: ${LATEST_VERSIONS[nextcloud]}"
echo " Traefik: ${LATEST_VERSIONS[traefik]}"
echo " Ubuntu: ${LATEST_VERSIONS[ubuntu]}"
echo ""
echo -e "${YELLOW}Note: ${RED}Red${NC} indicates outdated version${NC}"
echo ""
;;
csv)
# CSV header
echo "client,status,authentik,nextcloud,traefik,ubuntu,last_update,outdated"
# CSV rows
for client in $CLIENTS; do
status=$(yq eval ".clients.\"$client\".status" "$REGISTRY_FILE")
authentik=$(yq eval ".clients.\"$client\".versions.authentik" "$REGISTRY_FILE")
nextcloud=$(yq eval ".clients.\"$client\".versions.nextcloud" "$REGISTRY_FILE")
traefik=$(yq eval ".clients.\"$client\".versions.traefik" "$REGISTRY_FILE")
ubuntu=$(yq eval ".clients.\"$client\".versions.ubuntu" "$REGISTRY_FILE")
last_update=$(yq eval ".clients.\"$client\".maintenance.last_full_update" "$REGISTRY_FILE")
# Check if any version is outdated
outdated="no"
is_outdated "authentik" "$authentik" && outdated="yes"
is_outdated "nextcloud" "$nextcloud" && outdated="yes"
is_outdated "traefik" "$traefik" && outdated="yes"
is_outdated "ubuntu" "$ubuntu" && outdated="yes"
# Skip if filtering by outdated
if [ "$SHOW_OUTDATED" = true ] && [ "$outdated" = "no" ]; then
continue
fi
echo "$client,$status,$authentik,$nextcloud,$traefik,$ubuntu,$last_update,$outdated"
done
;;
json)
# Build JSON array
echo "{"
echo " \"latest_versions\": {"
echo " \"authentik\": \"${LATEST_VERSIONS[authentik]}\","
echo " \"nextcloud\": \"${LATEST_VERSIONS[nextcloud]}\","
echo " \"traefik\": \"${LATEST_VERSIONS[traefik]}\","
echo " \"ubuntu\": \"${LATEST_VERSIONS[ubuntu]}\""
echo " },"
echo " \"clients\": ["
first=true
for client in $CLIENTS; do
status=$(yq eval ".clients.\"$client\".status" "$REGISTRY_FILE")
authentik=$(yq eval ".clients.\"$client\".versions.authentik" "$REGISTRY_FILE")
nextcloud=$(yq eval ".clients.\"$client\".versions.nextcloud" "$REGISTRY_FILE")
traefik=$(yq eval ".clients.\"$client\".versions.traefik" "$REGISTRY_FILE")
ubuntu=$(yq eval ".clients.\"$client\".versions.ubuntu" "$REGISTRY_FILE")
last_update=$(yq eval ".clients.\"$client\".maintenance.last_full_update" "$REGISTRY_FILE")
# Check if any version is outdated
outdated=false
is_outdated "authentik" "$authentik" && outdated=true
is_outdated "nextcloud" "$nextcloud" && outdated=true
is_outdated "traefik" "$traefik" && outdated=true
is_outdated "ubuntu" "$ubuntu" && outdated=true
# Skip if filtering by outdated
if [ "$SHOW_OUTDATED" = true ] && [ "$outdated" = false ]; then
continue
fi
if [ "$first" = false ]; then
echo " ,"
fi
first=false
cat <<EOF
{
"name": "$client",
"status": "$status",
"versions": {
"authentik": "$authentik",
"nextcloud": "$nextcloud",
"traefik": "$traefik",
"ubuntu": "$ubuntu"
},
"last_update": "$last_update",
"outdated": $outdated
}
EOF
done
echo ""
echo " ]"
echo "}"
;;
*)
echo -e "${RED}Error: Unknown format '$FORMAT'${NC}"
echo "Valid formats: table, csv, json"
exit 1
;;
esac