feat: Implement client registry system (issue #12)

Add comprehensive client registry for tracking all deployed infrastructure:

Registry System:
- Single source of truth in clients/registry.yml
- Tracks status, server specs, versions, maintenance history
- Supports canary deployment workflow
- Automatic updates via deployment scripts

New Scripts:
- scripts/list-clients.sh: List/filter clients (table/json/csv/summary)
- scripts/client-status.sh: Detailed client info with health checks
- scripts/update-registry.sh: Manual registry updates

Updated Scripts:
- scripts/deploy-client.sh: Auto-updates registry on deploy
- scripts/rebuild-client.sh: Auto-updates registry on rebuild
- scripts/destroy-client.sh: Marks clients as destroyed

Documentation:
- docs/client-registry.md: Complete registry reference
- clients/README.md: Quick start guide

Status tracking: pending → deployed → maintenance → destroyed
Role support: canary (dev) and production clients

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pieter 2026-01-17 20:24:53 +01:00
parent ac4187d041
commit bf4659f662
9 changed files with 1237 additions and 8 deletions

97
clients/README.md Normal file
View file

@ -0,0 +1,97 @@
# Client Registry
This directory contains the client registry system for tracking all deployed infrastructure.
## Files
- **[registry.yml](registry.yml)** - Single source of truth for all clients
- Deployment status and lifecycle
- Server specifications
- Application versions
- Maintenance history
- Access URLs
## Management Scripts
All scripts are located in [`../scripts/`](../scripts/):
### View Clients
```bash
# List all clients
../scripts/list-clients.sh
# Filter by status
../scripts/list-clients.sh --status=deployed
# Filter by role
../scripts/list-clients.sh --role=canary
# Different formats
../scripts/list-clients.sh --format=table # Default
../scripts/list-clients.sh --format=json # JSON
../scripts/list-clients.sh --format=csv # CSV export
../scripts/list-clients.sh --format=summary # Statistics
```
### View Client Details
```bash
# Show detailed status with live health checks
../scripts/client-status.sh <client_name>
```
### Update Registry
The registry is **automatically updated** by deployment scripts:
- `deploy-client.sh` - Creates/updates entry on deployment
- `rebuild-client.sh` - Updates entry on rebuild
- `destroy-client.sh` - Marks as destroyed
For manual updates:
```bash
../scripts/update-registry.sh <client_name> <action> [options]
```
## Registry Structure
Each client entry tracks:
- **Status**: `pending``deployed``maintenance``offboarding``destroyed`
- **Role**: `canary` (testing) or `production` (live)
- **Server**: Type, location, IP, Hetzner ID
- **Apps**: Installed applications
- **Versions**: Application and OS versions
- **Maintenance**: Update and backup history
- **URLs**: Access endpoints
- **Notes**: Operational documentation
## Canary Deployment
The `dev` client has role `canary` and is used for testing:
```bash
# 1. Test on canary first
../scripts/deploy-client.sh dev
# 2. Verify it works
../scripts/client-status.sh dev
# 3. Roll out to production
for client in $(../scripts/list-clients.sh --role=production --format=csv | tail -n +2 | cut -d, -f1); do
../scripts/rebuild-client.sh "$client"
done
```
## Documentation
See [docs/client-registry.md](../docs/client-registry.md) for:
- Complete registry structure reference
- Management script usage
- Best practices
- Integration examples
- Troubleshooting guide
## Requirements
- **yq**: YAML processor (`brew install yq`)
- **jq**: JSON processor (`brew install jq`)

89
clients/registry.yml Normal file
View file

@ -0,0 +1,89 @@
# Client Registry
#
# Single source of truth for all clients in the infrastructure.
# This file tracks client lifecycle, deployment state, and versions.
#
# Status values:
# - pending: Client configuration created, not yet deployed
# - deployed: Client is live and operational
# - maintenance: Under maintenance, may be temporarily unavailable
# - offboarding: Being decommissioned
# - destroyed: Infrastructure removed, secrets archived
#
# Role values:
# - canary: Used for testing updates before production rollout
# - production: Live client serving real users
clients:
dev:
status: deployed
role: canary
deployed_date: 2026-01-17
destroyed_date: null
server:
type: cpx22 # 3 vCPU, 4 GB RAM, 80 GB SSD
location: fsn1 # Falkenstein, Germany
ip: 78.47.191.38
id: "117714358" # Hetzner server ID
apps:
- authentik
- nextcloud
versions:
authentik: "2025.10.3"
nextcloud: "30.0.17"
traefik: "v3.0"
ubuntu: "24.04"
maintenance:
last_full_update: 2026-01-17
last_security_patch: 2026-01-17
last_os_update: 2026-01-17
last_backup_verified: null
urls:
authentik: "https://auth.dev.vrije.cloud"
nextcloud: "https://nextcloud.dev.vrije.cloud"
notes: |
Canary/test server. Used for testing updates before production rollout.
Server was recreated on 2026-01-17 for per-client SSH key implementation.
# Add new clients here as they are deployed
# Template:
#
# clientname:
# status: deployed
# role: production
# deployed_date: YYYY-MM-DD
# destroyed_date: null
#
# server:
# type: cx22
# location: nbg1
# ip: 1.2.3.4
# id: "12345678"
#
# apps:
# - authentik
# - nextcloud
#
# versions:
# authentik: "2025.10.3"
# nextcloud: "30.0.17"
# traefik: "v3.0"
# ubuntu: "24.04"
#
# maintenance:
# last_full_update: YYYY-MM-DD
# last_security_patch: YYYY-MM-DD
# last_os_update: YYYY-MM-DD
# last_backup_verified: null
#
# urls:
# authentik: "https://auth.clientname.vrije.cloud"
# nextcloud: "https://nextcloud.clientname.vrije.cloud"
#
# notes: ""

325
docs/client-registry.md Normal file
View file

@ -0,0 +1,325 @@
# Client Registry
The client registry is the single source of truth for tracking all deployed clients, their configuration, status, and maintenance history.
## Overview
The registry is stored in [`clients/registry.yml`](../clients/registry.yml) and tracks:
- Deployment status and lifecycle
- Server specifications and location
- Installed applications and versions
- Maintenance history
- Access URLs
- Operational notes
## Registry Structure
```yaml
clients:
clientname:
status: deployed # pending | deployed | maintenance | offboarding | destroyed
role: production # canary | production
deployed_date: 2026-01-17
destroyed_date: null
server:
type: cx22 # Hetzner server type
location: nbg1 # Data center location
ip: 1.2.3.4
id: "12345678" # Hetzner server ID
apps:
- authentik
- nextcloud
versions:
authentik: "2025.10.3"
nextcloud: "30.0.17"
traefik: "v3.0"
ubuntu: "24.04"
maintenance:
last_full_update: 2026-01-17
last_security_patch: 2026-01-17
last_os_update: 2026-01-17
last_backup_verified: null
urls:
authentik: "https://auth.clientname.vrije.cloud"
nextcloud: "https://nextcloud.clientname.vrije.cloud"
notes: ""
```
## Status Values
- **pending**: Client configuration created, not yet deployed
- **deployed**: Client is live and operational
- **maintenance**: Under maintenance, may be temporarily unavailable
- **offboarding**: Being decommissioned
- **destroyed**: Infrastructure removed, secrets archived
## Role Values
- **canary**: Used for testing updates before production rollout (e.g., `dev`)
- **production**: Live client serving real users
## Management Scripts
### List All Clients
```bash
# List all clients in table format
./scripts/list-clients.sh
# Filter by status
./scripts/list-clients.sh --status=deployed
./scripts/list-clients.sh --status=destroyed
# Filter by role
./scripts/list-clients.sh --role=canary
./scripts/list-clients.sh --role=production
# Different output formats
./scripts/list-clients.sh --format=table # Default, colorized table
./scripts/list-clients.sh --format=json # JSON output
./scripts/list-clients.sh --format=csv # CSV export
./scripts/list-clients.sh --format=summary # Summary statistics
```
### View Client Details
```bash
# Show detailed status for a specific client
./scripts/client-status.sh dev
# Includes:
# - Deployment status and metadata
# - Server specifications
# - Application versions
# - Maintenance history
# - Access URLs
# - Live health checks (if deployed)
```
### Update Registry Manually
```bash
# Mark client as deployed
./scripts/update-registry.sh myclient deploy \
--role=production \
--server-ip=1.2.3.4 \
--server-id=12345678 \
--server-type=cx22 \
--server-location=nbg1
# Mark client as destroyed
./scripts/update-registry.sh myclient destroy
# Update status
./scripts/update-registry.sh myclient status --status=maintenance
```
## Automatic Updates
The registry is **automatically updated** by deployment scripts:
### Deploy Script
When running `./scripts/deploy-client.sh myclient`:
1. Creates registry entry if doesn't exist
2. Sets status to `deployed`
3. Records server details from OpenTofu state
4. Sets deployment date
5. Initializes maintenance tracking
### Rebuild Script
When running `./scripts/rebuild-client.sh myclient`:
1. Updates existing registry entry
2. Refreshes server details (IP, ID may change)
3. Updates `last_full_update` date
4. Maintains historical data
### Destroy Script
When running `./scripts/destroy-client.sh myclient`:
1. Sets status to `destroyed`
2. Records destruction date
3. Preserves all historical data
4. Keeps entry for audit trail
## Canary Deployment Workflow
The registry supports canary deployments for safe rollouts:
```bash
# 1. Test on canary server first
./scripts/deploy-client.sh dev
# 2. Verify canary is working
./scripts/client-status.sh dev
# 3. If successful, roll out to production
./scripts/list-clients.sh --role=production | while read client; do
./scripts/rebuild-client.sh "$client"
done
```
## Best Practices
### 1. Always Review Registry Before Changes
```bash
# Check current state
./scripts/list-clients.sh
# Review specific client
./scripts/client-status.sh myclient
```
### 2. Use Status Field for Coordination
Mark clients as `maintenance` before disruptive changes:
```bash
./scripts/update-registry.sh myclient status --status=maintenance
# Perform maintenance...
./scripts/update-registry.sh myclient status --status=deployed
```
### 3. Track Maintenance History
Update maintenance fields after significant operations:
```bash
# After security patches
yq eval -i ".clients.myclient.maintenance.last_security_patch = \"$(date +%Y-%m-%d)\"" clients/registry.yml
# After OS updates
yq eval -i ".clients.myclient.maintenance.last_os_update = \"$(date +%Y-%m-%d)\"" clients/registry.yml
# After backup verification
yq eval -i ".clients.myclient.maintenance.last_backup_verified = \"$(date +%Y-%m-%d)\"" clients/registry.yml
```
### 4. Add Operational Notes
Document important events:
```bash
yq eval -i ".clients.myclient.notes = \"Upgraded to Nextcloud 31 on 2026-01-20. Migration successful.\"" clients/registry.yml
```
### 5. Export for Reporting
```bash
# Generate CSV report for management
./scripts/list-clients.sh --format=csv > reports/clients-$(date +%Y%m%d).csv
# Get summary statistics
./scripts/list-clients.sh --format=summary
```
## Version Control
The registry is **version controlled** in Git:
- All changes are tracked
- Audit trail of client lifecycle
- Easy rollback if needed
- Collaborative management
Always commit registry changes:
```bash
git add clients/registry.yml
git commit -m "chore: Update client registry after deployment"
git push
```
## Querying with yq
For advanced queries, use `yq` directly:
```bash
# Find all deployed clients
yq eval '.clients | to_entries | map(select(.value.status == "deployed")) | .[].key' clients/registry.yml
# Find canary clients
yq eval '.clients | to_entries | map(select(.value.role == "canary")) | .[].key' clients/registry.yml
# Get all IPs
yq eval '.clients | to_entries | .[] | "\(.key): \(.value.server.ip)"' clients/registry.yml
# Find clients needing updates (no update in 30+ days)
# (requires date arithmetic with external tools)
```
## Integration with Monitoring
The registry can feed into monitoring systems:
```bash
# Export as JSON for consumption by monitoring tools
./scripts/list-clients.sh --format=json > /var/monitoring/clients.json
# Check health of all deployed clients
for client in $(./scripts/list-clients.sh --status=deployed --format=csv | tail -n +2 | cut -d, -f1); do
./scripts/client-status.sh "$client"
done
```
## Troubleshooting
### Registry Out of Sync
If registry doesn't match reality:
```bash
# Get actual state from OpenTofu
cd tofu
tofu state list
# Get actual server details
tofu state show 'hcloud_server.client["myclient"]'
# Update registry manually
./scripts/update-registry.sh myclient deploy \
--server-ip=<actual-ip> \
--server-id=<actual-id>
```
### Missing Registry Entry
If a client exists but not in registry:
```bash
# Create entry manually
./scripts/update-registry.sh myclient deploy
# Or rebuild to auto-create
./scripts/rebuild-client.sh myclient
```
### Corrupted Registry File
If YAML is invalid:
```bash
# Check syntax
yq eval . clients/registry.yml
# Restore from Git
git checkout clients/registry.yml
# Or restore from backup
cp clients/registry.yml.backup clients/registry.yml
```
## Related Documentation
- [SSH Key Management](ssh-key-management.md) - Per-client SSH keys
- [Secrets Management](../secrets/clients/README.md) - SOPS-encrypted secrets
- [Deployment Guide](deployment.md) - Full deployment procedures
- [Maintenance Guide](maintenance.md) - Update and patching procedures

237
scripts/client-status.sh Executable file
View file

@ -0,0 +1,237 @@
#!/usr/bin/env bash
#
# Show detailed status for a specific client
#
# Usage: ./scripts/client-status.sh <client_name>
#
# Displays:
# - Deployment status and metadata
# - Server information
# - Application versions
# - Maintenance history
# - URLs and access information
# - Live health checks (optional)
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"
# Check arguments
if [ $# -ne 1 ]; then
echo -e "${RED}Error: Client name required${NC}"
echo "Usage: $0 <client_name>"
echo ""
echo "Example: $0 dev"
exit 1
fi
CLIENT_NAME="$1"
# 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
# Check if client exists
if yq eval ".clients.\"$CLIENT_NAME\"" "$REGISTRY_FILE" | grep -q "null"; then
echo -e "${RED}Error: Client '$CLIENT_NAME' not found in registry${NC}"
echo ""
echo "Available clients:"
yq eval '.clients | keys | .[]' "$REGISTRY_FILE"
exit 1
fi
# Extract client information
STATUS=$(yq eval ".clients.\"$CLIENT_NAME\".status" "$REGISTRY_FILE")
ROLE=$(yq eval ".clients.\"$CLIENT_NAME\".role" "$REGISTRY_FILE")
DEPLOYED_DATE=$(yq eval ".clients.\"$CLIENT_NAME\".deployed_date" "$REGISTRY_FILE")
DESTROYED_DATE=$(yq eval ".clients.\"$CLIENT_NAME\".destroyed_date" "$REGISTRY_FILE")
SERVER_TYPE=$(yq eval ".clients.\"$CLIENT_NAME\".server.type" "$REGISTRY_FILE")
SERVER_LOCATION=$(yq eval ".clients.\"$CLIENT_NAME\".server.location" "$REGISTRY_FILE")
SERVER_IP=$(yq eval ".clients.\"$CLIENT_NAME\".server.ip" "$REGISTRY_FILE")
SERVER_ID=$(yq eval ".clients.\"$CLIENT_NAME\".server.id" "$REGISTRY_FILE")
APPS=$(yq eval ".clients.\"$CLIENT_NAME\".apps | join(\", \")" "$REGISTRY_FILE")
AUTHENTIK_VERSION=$(yq eval ".clients.\"$CLIENT_NAME\".versions.authentik" "$REGISTRY_FILE")
NEXTCLOUD_VERSION=$(yq eval ".clients.\"$CLIENT_NAME\".versions.nextcloud" "$REGISTRY_FILE")
TRAEFIK_VERSION=$(yq eval ".clients.\"$CLIENT_NAME\".versions.traefik" "$REGISTRY_FILE")
UBUNTU_VERSION=$(yq eval ".clients.\"$CLIENT_NAME\".versions.ubuntu" "$REGISTRY_FILE")
LAST_FULL_UPDATE=$(yq eval ".clients.\"$CLIENT_NAME\".maintenance.last_full_update" "$REGISTRY_FILE")
LAST_SECURITY_PATCH=$(yq eval ".clients.\"$CLIENT_NAME\".maintenance.last_security_patch" "$REGISTRY_FILE")
LAST_OS_UPDATE=$(yq eval ".clients.\"$CLIENT_NAME\".maintenance.last_os_update" "$REGISTRY_FILE")
LAST_BACKUP_VERIFIED=$(yq eval ".clients.\"$CLIENT_NAME\".maintenance.last_backup_verified" "$REGISTRY_FILE")
AUTHENTIK_URL=$(yq eval ".clients.\"$CLIENT_NAME\".urls.authentik" "$REGISTRY_FILE")
NEXTCLOUD_URL=$(yq eval ".clients.\"$CLIENT_NAME\".urls.nextcloud" "$REGISTRY_FILE")
NOTES=$(yq eval ".clients.\"$CLIENT_NAME\".notes" "$REGISTRY_FILE")
# Display header
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} CLIENT STATUS: $CLIENT_NAME${NC}"
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"
echo ""
# Status section
echo -e "${CYAN}━━━ Deployment Status ━━━${NC}"
echo ""
# Color status
STATUS_COLOR=$NC
case $STATUS in
deployed) STATUS_COLOR=$GREEN ;;
pending) STATUS_COLOR=$YELLOW ;;
maintenance) STATUS_COLOR=$CYAN ;;
offboarding) STATUS_COLOR=$RED ;;
destroyed) STATUS_COLOR=$RED ;;
esac
# Color role
ROLE_COLOR=$NC
case $ROLE in
canary) ROLE_COLOR=$YELLOW ;;
production) ROLE_COLOR=$GREEN ;;
esac
echo -e "Status: ${STATUS_COLOR}$STATUS${NC}"
echo -e "Role: ${ROLE_COLOR}$ROLE${NC}"
echo -e "Deployed: $DEPLOYED_DATE"
if [ "$DESTROYED_DATE" != "null" ]; then
echo -e "Destroyed: ${RED}$DESTROYED_DATE${NC}"
fi
echo ""
# Server section
echo -e "${CYAN}━━━ Server Information ━━━${NC}"
echo ""
echo -e "Server Type: $SERVER_TYPE"
echo -e "Location: $SERVER_LOCATION"
echo -e "IP Address: $SERVER_IP"
echo -e "Server ID: $SERVER_ID"
echo ""
# Applications section
echo -e "${CYAN}━━━ Applications ━━━${NC}"
echo ""
echo -e "Installed: $APPS"
echo ""
# Versions section
echo -e "${CYAN}━━━ Versions ━━━${NC}"
echo ""
echo -e "Authentik: $AUTHENTIK_VERSION"
echo -e "Nextcloud: $NEXTCLOUD_VERSION"
echo -e "Traefik: $TRAEFIK_VERSION"
echo -e "Ubuntu: $UBUNTU_VERSION"
echo ""
# Maintenance section
echo -e "${CYAN}━━━ Maintenance History ━━━${NC}"
echo ""
echo -e "Last Full Update: $LAST_FULL_UPDATE"
echo -e "Last Security Patch: $LAST_SECURITY_PATCH"
echo -e "Last OS Update: $LAST_OS_UPDATE"
if [ "$LAST_BACKUP_VERIFIED" != "null" ]; then
echo -e "Last Backup Verified: $LAST_BACKUP_VERIFIED"
else
echo -e "Last Backup Verified: ${YELLOW}Never${NC}"
fi
echo ""
# URLs section
echo -e "${CYAN}━━━ Access URLs ━━━${NC}"
echo ""
echo -e "Authentik: $AUTHENTIK_URL"
echo -e "Nextcloud: $NEXTCLOUD_URL"
echo ""
# Notes section
if [ "$NOTES" != "null" ] && [ -n "$NOTES" ]; then
echo -e "${CYAN}━━━ Notes ━━━${NC}"
echo ""
echo "$NOTES" | sed 's/^/ /'
echo ""
fi
# Live health check (if server is deployed and reachable)
if [ "$STATUS" = "deployed" ]; then
echo -e "${CYAN}━━━ Live Health Check ━━━${NC}"
echo ""
# Check if server is reachable via SSH (if Ansible is configured)
if command -v ansible &> /dev/null && [ -n "${HCLOUD_TOKEN:-}" ]; then
cd "$PROJECT_ROOT/ansible"
if timeout 10 ~/.local/bin/ansible -i hcloud.yml "$CLIENT_NAME" -m ping -o &>/dev/null; then
echo -e "SSH Access: ${GREEN}✓ Reachable${NC}"
# Get Docker status
DOCKER_STATUS=$(~/.local/bin/ansible -i hcloud.yml "$CLIENT_NAME" -m shell -a "docker ps --format '{{.Names}}' 2>/dev/null | wc -l" -o 2>/dev/null | tail -1 | awk '{print $NF}' || echo "0")
if [ "$DOCKER_STATUS" != "0" ]; then
echo -e "Docker: ${GREEN}✓ Running ($DOCKER_STATUS containers)${NC}"
else
echo -e "Docker: ${RED}✗ No containers running${NC}"
fi
else
echo -e "SSH Access: ${RED}✗ Not reachable${NC}"
fi
else
echo -e "${YELLOW}Note: Install Ansible and set HCLOUD_TOKEN for live health checks${NC}"
fi
echo ""
# Check HTTPS endpoints
echo -e "HTTPS Endpoints:"
# Check Authentik
if command -v curl &> /dev/null; then
if timeout 10 curl -sSf -o /dev/null "$AUTHENTIK_URL" 2>/dev/null; then
echo -e " Authentik: ${GREEN}✓ Responding${NC}"
else
echo -e " Authentik: ${RED}<EFBFBD><EFBFBD> Not responding${NC}"
fi
# Check Nextcloud
if timeout 10 curl -sSf -o /dev/null "$NEXTCLOUD_URL" 2>/dev/null; then
echo -e " Nextcloud: ${GREEN}✓ Responding${NC}"
else
echo -e " Nextcloud: ${RED}✗ Not responding${NC}"
fi
else
echo -e " ${YELLOW}Install curl for endpoint checks${NC}"
fi
echo ""
fi
# Management commands section
echo -e "${CYAN}━━━ Management Commands ━━━${NC}"
echo ""
echo -e "View secrets: ${BLUE}sops secrets/clients/${CLIENT_NAME}.sops.yaml${NC}"
echo -e "Rebuild server: ${BLUE}./scripts/rebuild-client.sh $CLIENT_NAME${NC}"
echo -e "Destroy server: ${BLUE}./scripts/destroy-client.sh $CLIENT_NAME${NC}"
echo -e "List all: ${BLUE}./scripts/list-clients.sh${NC}"
echo ""
echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}"

View file

@ -139,7 +139,7 @@ echo -e "${BLUE}========================================${NC}"
echo "" echo ""
# Step 1: Provision infrastructure # Step 1: Provision infrastructure
echo -e "${YELLOW}[1/3] Provisioning infrastructure with OpenTofu...${NC}" echo -e "${YELLOW}[1/4] Provisioning infrastructure with OpenTofu...${NC}"
cd "$PROJECT_ROOT/tofu" cd "$PROJECT_ROOT/tofu"
@ -163,7 +163,7 @@ fi
echo "" echo ""
# Step 2: Setup base system # Step 2: Setup base system
echo -e "${YELLOW}[2/3] Setting up base system (Docker, Traefik)...${NC}" echo -e "${YELLOW}[2/4] Setting up base system (Docker, Traefik)...${NC}"
cd "$PROJECT_ROOT/ansible" cd "$PROJECT_ROOT/ansible"
@ -174,7 +174,7 @@ echo -e "${GREEN}✓ Base system configured${NC}"
echo "" echo ""
# Step 3: Deploy applications # Step 3: Deploy applications
echo -e "${YELLOW}[3/3] Deploying applications (Authentik, Nextcloud, SSO)...${NC}" echo -e "${YELLOW}[3/4] Deploying applications (Authentik, Nextcloud, SSO)...${NC}"
~/.local/bin/ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit "$CLIENT_NAME" ~/.local/bin/ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit "$CLIENT_NAME"
@ -182,6 +182,35 @@ echo ""
echo -e "${GREEN}✓ Applications deployed${NC}" echo -e "${GREEN}✓ Applications deployed${NC}"
echo "" echo ""
# Step 4: Update client registry
echo -e "${YELLOW}[4/4] Updating client registry...${NC}"
cd "$PROJECT_ROOT/tofu"
# Get server information from Terraform state
SERVER_IP=$(tofu output -json client_ips 2>/dev/null | jq -r ".\"$CLIENT_NAME\"" || echo "")
SERVER_ID=$(tofu state show "hcloud_server.client[\"$CLIENT_NAME\"]" 2>/dev/null | grep "^[[:space:]]*id[[:space:]]*=" | awk '{print $3}' | tr -d '"' || echo "")
SERVER_TYPE=$(tofu state show "hcloud_server.client[\"$CLIENT_NAME\"]" 2>/dev/null | grep "^[[:space:]]*server_type[[:space:]]*=" | awk '{print $3}' | tr -d '"' || echo "")
SERVER_LOCATION=$(tofu state show "hcloud_server.client[\"$CLIENT_NAME\"]" 2>/dev/null | grep "^[[:space:]]*location[[:space:]]*=" | awk '{print $3}' | tr -d '"' || echo "")
# Determine role (dev is canary, everything else is production by default)
ROLE="production"
if [ "$CLIENT_NAME" = "dev" ]; then
ROLE="canary"
fi
# Update registry
"$SCRIPT_DIR/update-registry.sh" "$CLIENT_NAME" deploy \
--role="$ROLE" \
--server-ip="$SERVER_IP" \
--server-id="$SERVER_ID" \
--server-type="$SERVER_TYPE" \
--server-location="$SERVER_LOCATION"
echo ""
echo -e "${GREEN}✓ Registry updated${NC}"
echo ""
# Calculate duration # Calculate duration
END_TIME=$(date +%s) END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME)) DURATION=$((END_TIME - START_TIME))

View file

@ -113,7 +113,7 @@ fi
echo "" echo ""
# Step 3: Destroy infrastructure with OpenTofu # Step 3: Destroy infrastructure with OpenTofu
echo -e "${YELLOW}[3/3] Destroying infrastructure with OpenTofu...${NC}" echo -e "${YELLOW}[3/4] Destroying infrastructure with OpenTofu...${NC}"
cd "$PROJECT_ROOT/tofu" cd "$PROJECT_ROOT/tofu"
@ -128,6 +128,15 @@ tofu apply destroy.tfplan
# Cleanup plan file # Cleanup plan file
rm -f destroy.tfplan rm -f destroy.tfplan
echo ""
# Step 4: Update client registry
echo -e "${YELLOW}[4/4] Updating client registry...${NC}"
"$SCRIPT_DIR/update-registry.sh" "$CLIENT_NAME" destroy
echo ""
echo -e "${GREEN}✓ Registry updated${NC}"
echo "" echo ""
echo -e "${GREEN}========================================${NC}" echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}✓ Client '$CLIENT_NAME' destroyed successfully${NC}" echo -e "${GREEN}✓ Client '$CLIENT_NAME' destroyed successfully${NC}"

231
scripts/list-clients.sh Executable file
View file

@ -0,0 +1,231 @@
#!/usr/bin/env bash
#
# List all clients from the registry
#
# Usage: ./scripts/list-clients.sh [--status=<status>] [--role=<role>] [--format=<format>]
#
# Options:
# --status=<status> Filter by status (deployed, pending, maintenance, offboarding, destroyed)
# --role=<role> Filter by role (canary, production)
# --format=<format> Output format: table (default), json, csv, summary
#
# Examples:
# ./scripts/list-clients.sh # List all clients
# ./scripts/list-clients.sh --status=deployed # Only deployed clients
# ./scripts/list-clients.sh --role=production # Only production clients
# ./scripts/list-clients.sh --format=json # JSON output
set -euo pipefail
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
REGISTRY_FILE="$PROJECT_ROOT/clients/registry.yml"
# Colors
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
# Parse arguments
FILTER_STATUS=""
FILTER_ROLE=""
FORMAT="table"
for arg in "$@"; do
case $arg in
--status=*)
FILTER_STATUS="${arg#*=}"
;;
--role=*)
FILTER_ROLE="${arg#*=}"
;;
--format=*)
FORMAT="${arg#*=}"
;;
--help|-h)
echo "Usage: $0 [--status=<status>] [--role=<role>] [--format=<format>]"
echo ""
echo "Options:"
echo " --status=<status> Filter by status (deployed, pending, maintenance, offboarding, destroyed)"
echo " --role=<role> Filter by role (canary, production)"
echo " --format=<format> Output format: table (default), json, csv, summary"
exit 0
;;
esac
done
# Check if registry exists
if [ ! -f "$REGISTRY_FILE" ]; then
echo -e "${RED}Error: Registry file not found: $REGISTRY_FILE${NC}"
exit 1
fi
# Check if yq is available (for YAML parsing)
if ! command -v yq &> /dev/null; then
echo -e "${YELLOW}Warning: 'yq' not found. Install with: brew install yq${NC}"
echo "Falling back to basic grep parsing..."
USE_YQ=false
else
USE_YQ=true
fi
# Function to get clients using yq
list_clients_yq() {
local clients=$(yq eval '.clients | keys | .[]' "$REGISTRY_FILE")
for client in $clients; do
local status=$(yq eval ".clients.\"$client\".status" "$REGISTRY_FILE")
local role=$(yq eval ".clients.\"$client\".role" "$REGISTRY_FILE")
# Apply filters
if [ -n "$FILTER_STATUS" ] && [ "$status" != "$FILTER_STATUS" ]; then
continue
fi
if [ -n "$FILTER_ROLE" ] && [ "$role" != "$FILTER_ROLE" ]; then
continue
fi
# Get other fields
local deployed_date=$(yq eval ".clients.\"$client\".deployed_date" "$REGISTRY_FILE")
local server_ip=$(yq eval ".clients.\"$client\".server.ip" "$REGISTRY_FILE")
local server_type=$(yq eval ".clients.\"$client\".server.type" "$REGISTRY_FILE")
local apps=$(yq eval ".clients.\"$client\".apps | join(\", \")" "$REGISTRY_FILE")
echo "$client|$status|$role|$deployed_date|$server_type|$server_ip|$apps"
done
}
# Function to output in table format
output_table() {
echo -e "${BLUE}╔════════════════════════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ CLIENT REGISTRY ║${NC}"
echo -e "${BLUE}╠════════════════════════════════════════════════════════════════════════════════════╣${NC}"
printf "${CYAN}%-15s ${GREEN}%-12s ${YELLOW}%-10s ${NC}%-12s %-10s %-15s %-20s\n" \
"CLIENT" "STATUS" "ROLE" "DEPLOYED" "TYPE" "IP" "APPS"
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────────────────${NC}"
local count=0
while IFS='|' read -r client status role deployed_date server_type server_ip apps; do
# Color status
local status_color=$NC
case $status in
deployed) status_color=$GREEN ;;
pending) status_color=$YELLOW ;;
maintenance) status_color=$CYAN ;;
offboarding) status_color=$RED ;;
destroyed) status_color=$RED ;;
esac
# Color role
local role_color=$NC
case $role in
canary) role_color=$YELLOW ;;
production) role_color=$GREEN ;;
esac
printf "%-15s ${status_color}%-12s${NC} ${role_color}%-10s${NC} %-12s %-10s %-15s %-20s\n" \
"$client" "$status" "$role" "$deployed_date" "$server_type" "$server_ip" "${apps:0:20}"
((count++))
done
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────────────────${NC}"
echo -e "${BLUE}${NC} Total clients: $count ${BLUE}${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════════════════════════════════╝${NC}"
}
# Function to output summary
output_summary() {
local total=0
local deployed=0
local pending=0
local maintenance=0
local canary=0
local production=0
while IFS='|' read -r client status role deployed_date server_type server_ip apps; do
((total++))
case $status in
deployed) ((deployed++)) ;;
pending) ((pending++)) ;;
maintenance) ((maintenance++)) ;;
esac
case $role in
canary) ((canary++)) ;;
production) ((production++)) ;;
esac
done
echo -e "${BLUE}═══════════════════════════════════${NC}"
echo -e "${BLUE} CLIENT REGISTRY SUMMARY${NC}"
echo -e "${BLUE}═══════════════════════════════════${NC}"
echo ""
echo -e "Total Clients: ${CYAN}$total${NC}"
echo ""
echo -e "By Status:"
echo -e " Deployed: ${GREEN}$deployed${NC}"
echo -e " Pending: ${YELLOW}$pending${NC}"
echo -e " Maintenance: ${CYAN}$maintenance${NC}"
echo ""
echo -e "By Role:"
echo -e " Canary: ${YELLOW}$canary${NC}"
echo -e " Production: ${GREEN}$production${NC}"
echo ""
}
# Function to output JSON
output_json() {
if $USE_YQ; then
yq eval -o=json '.clients' "$REGISTRY_FILE"
else
echo "{}"
fi
}
# Function to output CSV
output_csv() {
echo "client,status,role,deployed_date,server_type,server_ip,apps"
while IFS='|' read -r client status role deployed_date server_type server_ip apps; do
echo "$client,$status,$role,$deployed_date,$server_type,$server_ip,\"$apps\""
done
}
# Main execution
if $USE_YQ; then
DATA=$(list_clients_yq)
else
echo -e "${RED}Error: yq is required for this script${NC}"
echo "Install with: brew install yq"
exit 1
fi
# Check if any clients found
if [ -z "$DATA" ]; then
echo -e "${YELLOW}No clients found matching criteria${NC}"
exit 0
fi
# Output based on format
case $FORMAT in
table)
echo "$DATA" | output_table
;;
json)
output_json
;;
csv)
echo "$DATA" | output_csv
;;
summary)
echo "$DATA" | output_summary
;;
*)
echo -e "${RED}Unknown format: $FORMAT${NC}"
echo "Valid formats: table, json, csv, summary"
exit 1
;;
esac

View file

@ -118,7 +118,7 @@ echo -e "${BLUE}========================================${NC}"
echo "" echo ""
# Step 1: Check if infrastructure exists and destroy it # Step 1: Check if infrastructure exists and destroy it
echo -e "${YELLOW}[1/4] Checking existing infrastructure...${NC}" echo -e "${YELLOW}[1/5] Checking existing infrastructure...${NC}"
cd "$PROJECT_ROOT/tofu" cd "$PROJECT_ROOT/tofu"
@ -146,7 +146,7 @@ fi
echo "" echo ""
# Step 2: Provision infrastructure # Step 2: Provision infrastructure
echo -e "${YELLOW}[2/4] Provisioning infrastructure with OpenTofu...${NC}" echo -e "${YELLOW}[2/5] Provisioning infrastructure with OpenTofu...${NC}"
cd "$PROJECT_ROOT/tofu" cd "$PROJECT_ROOT/tofu"
@ -164,7 +164,7 @@ sleep 60
echo "" echo ""
# Step 3: Setup base system # Step 3: Setup base system
echo -e "${YELLOW}[3/4] Setting up base system (Docker, Traefik)...${NC}" echo -e "${YELLOW}[3/5] Setting up base system (Docker, Traefik)...${NC}"
cd "$PROJECT_ROOT/ansible" cd "$PROJECT_ROOT/ansible"
@ -175,7 +175,7 @@ echo -e "${GREEN}✓ Base system configured${NC}"
echo "" echo ""
# Step 4: Deploy applications # Step 4: Deploy applications
echo -e "${YELLOW}[4/4] Deploying applications (Authentik, Nextcloud, SSO)...${NC}" echo -e "${YELLOW}[4/5] Deploying applications (Authentik, Nextcloud, SSO)...${NC}"
~/.local/bin/ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit "$CLIENT_NAME" ~/.local/bin/ansible-playbook -i hcloud.yml playbooks/deploy.yml --limit "$CLIENT_NAME"
@ -183,6 +183,35 @@ echo ""
echo -e "${GREEN}✓ Applications deployed${NC}" echo -e "${GREEN}✓ Applications deployed${NC}"
echo "" echo ""
# Step 5: Update client registry
echo -e "${YELLOW}[5/5] Updating client registry...${NC}"
cd "$PROJECT_ROOT/tofu"
# Get server information from Terraform state
SERVER_IP=$(tofu output -json client_ips 2>/dev/null | jq -r ".\"$CLIENT_NAME\"" || echo "")
SERVER_ID=$(tofu state show "hcloud_server.client[\"$CLIENT_NAME\"]" 2>/dev/null | grep "^[[:space:]]*id[[:space:]]*=" | awk '{print $3}' | tr -d '"' || echo "")
SERVER_TYPE=$(tofu state show "hcloud_server.client[\"$CLIENT_NAME\"]" 2>/dev/null | grep "^[[:space:]]*server_type[[:space:]]*=" | awk '{print $3}' | tr -d '"' || echo "")
SERVER_LOCATION=$(tofu state show "hcloud_server.client[\"$CLIENT_NAME\"]" 2>/dev/null | grep "^[[:space:]]*location[[:space:]]*=" | awk '{print $3}' | tr -d '"' || echo "")
# Determine role (dev is canary, everything else is production by default)
ROLE="production"
if [ "$CLIENT_NAME" = "dev" ]; then
ROLE="canary"
fi
# Update registry
"$SCRIPT_DIR/update-registry.sh" "$CLIENT_NAME" deploy \
--role="$ROLE" \
--server-ip="$SERVER_IP" \
--server-id="$SERVER_ID" \
--server-type="$SERVER_TYPE" \
--server-location="$SERVER_LOCATION"
echo ""
echo -e "${GREEN}✓ Registry updated${NC}"
echo ""
# Calculate duration # Calculate duration
END_TIME=$(date +%s) END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME)) DURATION=$((END_TIME - START_TIME))

183
scripts/update-registry.sh Executable file
View file

@ -0,0 +1,183 @@
#!/usr/bin/env bash
#
# Update the client registry with deployment information
#
# Usage: ./scripts/update-registry.sh <client_name> <action> [options]
#
# Actions:
# deploy - Mark client as deployed (creates/updates entry)
# destroy - Mark client as destroyed
# status - Update status field
#
# Options:
# --status=<status> Set status (pending|deployed|maintenance|offboarding|destroyed)
# --role=<role> Set role (canary|production)
# --server-ip=<ip> Set server IP
# --server-id=<id> Set server ID
# --server-type=<type> Set server type
# --server-location=<loc> Set server location
set -euo pipefail
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
REGISTRY_FILE="$PROJECT_ROOT/clients/registry.yml"
# Check if yq is available
if ! command -v yq &> /dev/null; then
echo "Error: 'yq' not found. Install with: brew install yq"
exit 1
fi
# Parse arguments
if [ $# -lt 2 ]; then
echo "Usage: $0 <client_name> <action> [options]"
exit 1
fi
CLIENT_NAME="$1"
ACTION="$2"
shift 2
# Parse options
STATUS=""
ROLE=""
SERVER_IP=""
SERVER_ID=""
SERVER_TYPE=""
SERVER_LOCATION=""
for arg in "$@"; do
case $arg in
--status=*)
STATUS="${arg#*=}"
;;
--role=*)
ROLE="${arg#*=}"
;;
--server-ip=*)
SERVER_IP="${arg#*=}"
;;
--server-id=*)
SERVER_ID="${arg#*=}"
;;
--server-type=*)
SERVER_TYPE="${arg#*=}"
;;
--server-location=*)
SERVER_LOCATION="${arg#*=}"
;;
esac
done
# Ensure registry file exists
if [ ! -f "$REGISTRY_FILE" ]; then
cat > "$REGISTRY_FILE" <<'EOF'
# Client Registry
#
# Single source of truth for all clients in the infrastructure.
clients: {}
EOF
fi
TODAY=$(date +%Y-%m-%d)
case $ACTION in
deploy)
# Check if client exists
if yq eval ".clients.\"$CLIENT_NAME\"" "$REGISTRY_FILE" | grep -q "null"; then
# Create new client entry
echo "Creating new registry entry for $CLIENT_NAME"
# Start with minimal structure
yq eval -i ".clients.\"$CLIENT_NAME\" = {}" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".status = \"deployed\"" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".deployed_date = \"$TODAY\"" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".destroyed_date = null" "$REGISTRY_FILE"
# Add role
if [ -n "$ROLE" ]; then
yq eval -i ".clients.\"$CLIENT_NAME\".role = \"$ROLE\"" "$REGISTRY_FILE"
else
yq eval -i ".clients.\"$CLIENT_NAME\".role = \"production\"" "$REGISTRY_FILE"
fi
# Add server info
yq eval -i ".clients.\"$CLIENT_NAME\".server = {}" "$REGISTRY_FILE"
[ -n "$SERVER_TYPE" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.type = \"$SERVER_TYPE\"" "$REGISTRY_FILE"
[ -n "$SERVER_LOCATION" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.location = \"$SERVER_LOCATION\"" "$REGISTRY_FILE"
[ -n "$SERVER_IP" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.ip = \"$SERVER_IP\"" "$REGISTRY_FILE"
[ -n "$SERVER_ID" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.id = \"$SERVER_ID\"" "$REGISTRY_FILE"
# Add apps
yq eval -i ".clients.\"$CLIENT_NAME\".apps = [\"authentik\", \"nextcloud\"]" "$REGISTRY_FILE"
# Add maintenance tracking
yq eval -i ".clients.\"$CLIENT_NAME\".maintenance = {}" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".maintenance.last_full_update = \"$TODAY\"" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".maintenance.last_security_patch = \"$TODAY\"" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".maintenance.last_os_update = \"$TODAY\"" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".maintenance.last_backup_verified = null" "$REGISTRY_FILE"
# Add URLs (will be determined from secrets file)
yq eval -i ".clients.\"$CLIENT_NAME\".urls = {}" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".urls.authentik = \"https://auth.$CLIENT_NAME.vrije.cloud\"" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".urls.nextcloud = \"https://nextcloud.$CLIENT_NAME.vrije.cloud\"" "$REGISTRY_FILE"
# Add notes
yq eval -i ".clients.\"$CLIENT_NAME\".notes = \"\"" "$REGISTRY_FILE"
else
# Update existing client
echo "Updating registry entry for $CLIENT_NAME"
yq eval -i ".clients.\"$CLIENT_NAME\".status = \"deployed\"" "$REGISTRY_FILE"
# Update server info if provided
[ -n "$SERVER_IP" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.ip = \"$SERVER_IP\"" "$REGISTRY_FILE"
[ -n "$SERVER_ID" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.id = \"$SERVER_ID\"" "$REGISTRY_FILE"
[ -n "$SERVER_TYPE" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.type = \"$SERVER_TYPE\"" "$REGISTRY_FILE"
[ -n "$SERVER_LOCATION" ] && yq eval -i ".clients.\"$CLIENT_NAME\".server.location = \"$SERVER_LOCATION\"" "$REGISTRY_FILE"
# Update maintenance date
yq eval -i ".clients.\"$CLIENT_NAME\".maintenance.last_full_update = \"$TODAY\"" "$REGISTRY_FILE"
fi
;;
destroy)
echo "Marking $CLIENT_NAME as destroyed in registry"
if yq eval ".clients.\"$CLIENT_NAME\"" "$REGISTRY_FILE" | grep -q "null"; then
echo "Warning: Client $CLIENT_NAME not found in registry"
exit 0
fi
yq eval -i ".clients.\"$CLIENT_NAME\".status = \"destroyed\"" "$REGISTRY_FILE"
yq eval -i ".clients.\"$CLIENT_NAME\".destroyed_date = \"$TODAY\"" "$REGISTRY_FILE"
;;
status)
if [ -z "$STATUS" ]; then
echo "Error: --status=<status> required for status action"
exit 1
fi
echo "Updating status of $CLIENT_NAME to $STATUS"
if yq eval ".clients.\"$CLIENT_NAME\"" "$REGISTRY_FILE" | grep -q "null"; then
echo "Error: Client $CLIENT_NAME not found in registry"
exit 1
fi
yq eval -i ".clients.\"$CLIENT_NAME\".status = \"$STATUS\"" "$REGISTRY_FILE"
;;
*)
echo "Error: Unknown action '$ACTION'"
echo "Valid actions: deploy, destroy, status"
exit 1
;;
esac
echo "✓ Registry updated successfully"