2026-01-17 21:34:05 +01:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
#
|
|
|
|
|
# Add a new client to OpenTofu configuration
|
|
|
|
|
#
|
|
|
|
|
# Usage: ./scripts/add-client-to-terraform.sh <client_name> [options]
|
|
|
|
|
#
|
|
|
|
|
# Options:
|
|
|
|
|
# --server-type=TYPE Server type (default: cpx22)
|
|
|
|
|
# --location=LOC Data center location (default: fsn1)
|
|
|
|
|
# --volume-size=SIZE Nextcloud volume size in GB (default: 100)
|
|
|
|
|
# --apps=APP1,APP2 Applications to deploy (default: zitadel,nextcloud)
|
|
|
|
|
# --non-interactive Don't prompt, use defaults
|
|
|
|
|
|
|
|
|
|
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")"
|
|
|
|
|
TFVARS_FILE="$PROJECT_ROOT/tofu/terraform.tfvars"
|
|
|
|
|
|
|
|
|
|
# Check arguments
|
|
|
|
|
if [ $# -lt 1 ]; then
|
|
|
|
|
echo -e "${RED}Error: Client name required${NC}"
|
|
|
|
|
echo "Usage: $0 <client_name> [options]"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Options:"
|
|
|
|
|
echo " --server-type=TYPE Server type (default: cpx22)"
|
|
|
|
|
echo " --location=LOC Data center (default: fsn1)"
|
|
|
|
|
echo " --volume-size=SIZE Nextcloud volume GB (default: 100)"
|
|
|
|
|
echo " --apps=APP1,APP2 Apps (default: zitadel,nextcloud)"
|
|
|
|
|
echo " --non-interactive Use defaults, don't prompt"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Example: $0 blue --server-type=cx22 --location=nbg1 --volume-size=50"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
CLIENT_NAME="$1"
|
|
|
|
|
shift
|
|
|
|
|
|
|
|
|
|
# Default values
|
|
|
|
|
SERVER_TYPE="cpx22"
|
|
|
|
|
LOCATION="fsn1"
|
|
|
|
|
VOLUME_SIZE="100"
|
2026-01-20 19:06:32 +01:00
|
|
|
APPS="authentik,nextcloud"
|
|
|
|
|
PRIVATE_IP=""
|
|
|
|
|
PUBLIC_IP_ENABLED="false"
|
2026-01-17 21:34:05 +01:00
|
|
|
NON_INTERACTIVE=false
|
|
|
|
|
|
|
|
|
|
# Parse options
|
|
|
|
|
for arg in "$@"; do
|
|
|
|
|
case $arg in
|
|
|
|
|
--server-type=*)
|
|
|
|
|
SERVER_TYPE="${arg#*=}"
|
|
|
|
|
;;
|
|
|
|
|
--location=*)
|
|
|
|
|
LOCATION="${arg#*=}"
|
|
|
|
|
;;
|
|
|
|
|
--volume-size=*)
|
|
|
|
|
VOLUME_SIZE="${arg#*=}"
|
|
|
|
|
;;
|
|
|
|
|
--apps=*)
|
|
|
|
|
APPS="${arg#*=}"
|
|
|
|
|
;;
|
2026-01-20 19:06:32 +01:00
|
|
|
--private-ip=*)
|
|
|
|
|
PRIVATE_IP="${arg#*=}"
|
|
|
|
|
;;
|
|
|
|
|
--public-ip)
|
|
|
|
|
PUBLIC_IP_ENABLED="true"
|
|
|
|
|
;;
|
2026-01-17 21:34:05 +01:00
|
|
|
--non-interactive)
|
|
|
|
|
NON_INTERACTIVE=true
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
echo -e "${RED}Unknown option: $arg${NC}"
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
2026-01-20 19:06:32 +01:00
|
|
|
# Auto-assign private IP if not provided
|
|
|
|
|
if [ -z "$PRIVATE_IP" ]; then
|
|
|
|
|
# Find the highest existing IP in terraform.tfvars and increment
|
|
|
|
|
LAST_IP=$(grep -oP 'private_ip\s*=\s*"10\.0\.0\.\K\d+' "$TFVARS_FILE" 2>/dev/null | sort -n | tail -1)
|
|
|
|
|
if [ -z "$LAST_IP" ]; then
|
|
|
|
|
NEXT_IP=40 # Start from 10.0.0.40 (edge is .2)
|
|
|
|
|
else
|
|
|
|
|
NEXT_IP=$((LAST_IP + 1))
|
|
|
|
|
fi
|
|
|
|
|
PRIVATE_IP="10.0.0.$NEXT_IP"
|
|
|
|
|
echo -e "${BLUE}Auto-assigned private IP: $PRIVATE_IP${NC}"
|
|
|
|
|
echo ""
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-17 21:34:05 +01:00
|
|
|
# Validate client name
|
|
|
|
|
if [[ ! "$CLIENT_NAME" =~ ^[a-z0-9-]+$ ]]; then
|
|
|
|
|
echo -e "${RED}Error: Client name must contain only lowercase letters, numbers, and hyphens${NC}"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check if tfvars file exists
|
|
|
|
|
if [ ! -f "$TFVARS_FILE" ]; then
|
|
|
|
|
echo -e "${RED}Error: terraform.tfvars not found at $TFVARS_FILE${NC}"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check if client already exists
|
|
|
|
|
if grep -q "^[[:space:]]*${CLIENT_NAME}[[:space:]]*=" "$TFVARS_FILE"; then
|
|
|
|
|
echo -e "${YELLOW}⚠ Client '${CLIENT_NAME}' already exists in terraform.tfvars${NC}"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Existing configuration:"
|
|
|
|
|
grep -A 7 "^[[:space:]]*${CLIENT_NAME}[[:space:]]*=" "$TFVARS_FILE" | head -8
|
|
|
|
|
echo ""
|
|
|
|
|
read -p "Update configuration? (yes/no): " confirm
|
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
|
|
|
echo "Cancelled"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Remove existing entry
|
|
|
|
|
# This is complex - for now just error and let user handle manually
|
|
|
|
|
echo -e "${RED}Error: Updating existing clients not yet implemented${NC}"
|
|
|
|
|
echo "Please manually edit $TFVARS_FILE"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Interactive prompts (if not non-interactive)
|
|
|
|
|
if [ "$NON_INTERACTIVE" = false ]; then
|
|
|
|
|
echo -e "${BLUE}Adding client '${CLIENT_NAME}' to OpenTofu configuration${NC}"
|
|
|
|
|
echo ""
|
|
|
|
|
echo "Current defaults:"
|
|
|
|
|
echo " Server type: $SERVER_TYPE"
|
|
|
|
|
echo " Location: $LOCATION"
|
|
|
|
|
echo " Volume size: $VOLUME_SIZE GB"
|
|
|
|
|
echo " Apps: $APPS"
|
|
|
|
|
echo ""
|
|
|
|
|
read -p "Use these defaults? (yes/no): " use_defaults
|
|
|
|
|
|
|
|
|
|
if [ "$use_defaults" != "yes" ]; then
|
|
|
|
|
# Prompt for each value
|
|
|
|
|
echo ""
|
|
|
|
|
read -p "Server type [$SERVER_TYPE]: " input
|
|
|
|
|
SERVER_TYPE="${input:-$SERVER_TYPE}"
|
|
|
|
|
|
|
|
|
|
read -p "Location [$LOCATION]: " input
|
|
|
|
|
LOCATION="${input:-$LOCATION}"
|
|
|
|
|
|
|
|
|
|
read -p "Volume size GB [$VOLUME_SIZE]: " input
|
|
|
|
|
VOLUME_SIZE="${input:-$VOLUME_SIZE}"
|
|
|
|
|
|
|
|
|
|
read -p "Apps (comma-separated) [$APPS]: " input
|
|
|
|
|
APPS="${input:-$APPS}"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Convert apps list to array format
|
|
|
|
|
APPS_ARRAY=$(echo "$APPS" | sed 's/,/", "/g' | sed 's/^/["/' | sed 's/$/"]/')
|
|
|
|
|
|
|
|
|
|
# Find the closing brace of the clients block
|
|
|
|
|
CLIENTS_CLOSE_LINE=$(grep -n "^}" "$TFVARS_FILE" | head -1 | cut -d: -f1)
|
|
|
|
|
|
|
|
|
|
if [ -z "$CLIENTS_CLOSE_LINE" ]; then
|
|
|
|
|
echo -e "${RED}Error: Could not find closing brace in terraform.tfvars${NC}"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Create the new client configuration
|
|
|
|
|
NEW_CLIENT_CONFIG="
|
|
|
|
|
# ${CLIENT_NAME} server
|
|
|
|
|
${CLIENT_NAME} = {
|
|
|
|
|
server_type = \"${SERVER_TYPE}\"
|
|
|
|
|
location = \"${LOCATION}\"
|
|
|
|
|
subdomain = \"${CLIENT_NAME}\"
|
|
|
|
|
apps = ${APPS_ARRAY}
|
|
|
|
|
nextcloud_volume_size = ${VOLUME_SIZE}
|
2026-01-20 19:06:32 +01:00
|
|
|
private_ip = \"${PRIVATE_IP}\"
|
|
|
|
|
public_ip_enabled = ${PUBLIC_IP_ENABLED}
|
2026-01-17 21:34:05 +01:00
|
|
|
}"
|
|
|
|
|
|
|
|
|
|
# Create temporary file with new config inserted before closing brace
|
|
|
|
|
TMP_FILE=$(mktemp)
|
|
|
|
|
head -n $((CLIENTS_CLOSE_LINE - 1)) "$TFVARS_FILE" > "$TMP_FILE"
|
|
|
|
|
echo "$NEW_CLIENT_CONFIG" >> "$TMP_FILE"
|
|
|
|
|
tail -n +$CLIENTS_CLOSE_LINE "$TFVARS_FILE" >> "$TMP_FILE"
|
|
|
|
|
|
|
|
|
|
# Show the diff
|
|
|
|
|
echo ""
|
|
|
|
|
echo -e "${CYAN}Configuration to be added:${NC}"
|
|
|
|
|
echo "$NEW_CLIENT_CONFIG"
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
# Confirm
|
|
|
|
|
if [ "$NON_INTERACTIVE" = false ]; then
|
|
|
|
|
read -p "Add this configuration to terraform.tfvars? (yes/no): " confirm
|
|
|
|
|
if [ "$confirm" != "yes" ]; then
|
|
|
|
|
rm "$TMP_FILE"
|
|
|
|
|
echo "Cancelled"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Apply changes
|
|
|
|
|
mv "$TMP_FILE" "$TFVARS_FILE"
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo -e "${GREEN}✓ Client '${CLIENT_NAME}' added to terraform.tfvars${NC}"
|
|
|
|
|
echo ""
|
chore: Post-workshop state - January 23rd, 2026
This commit captures the infrastructure state immediately following
the "Post-Tyranny Tech" workshop on January 23rd, 2026.
Infrastructure Status:
- 13 client servers deployed (white, valk, zwaan, specht, das, uil, vos,
haas, wolf, ree, mees, mus, mol, kikker)
- Services: Authentik SSO, Nextcloud, Collabora Office, Traefik
- Private network architecture with edge NAT gateway
- OIDC integration between Authentik and Nextcloud
- Automated recovery flows and invitation system
- Container update monitoring with Diun
- Uptime monitoring with Uptime Kuma
Changes include:
- Multiple new client host configurations
- Network architecture improvements (private IPs + NAT)
- DNS management automation
- Container update notifications
- Email configuration via Mailgun
- SSH key generation for all clients
- Encrypted secrets for all deployments
- Health check and diagnostic scripts
Known Issues to Address:
- Nextcloud version pinned to v30 (should use 'latest' or v32)
- Zitadel references in templates (migrated to Authentik but templates not updated)
- Traefik dynamic config has obsolete static routes
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 20:36:31 +01:00
|
|
|
|
|
|
|
|
# Create Ansible host_vars file
|
|
|
|
|
HOST_VARS_FILE="$PROJECT_ROOT/ansible/host_vars/${CLIENT_NAME}.yml"
|
|
|
|
|
if [ ! -f "$HOST_VARS_FILE" ]; then
|
|
|
|
|
echo -e "${BLUE}Creating Ansible host_vars file...${NC}"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$(dirname "$HOST_VARS_FILE")"
|
|
|
|
|
|
|
|
|
|
cat > "$HOST_VARS_FILE" << EOF
|
|
|
|
|
---
|
|
|
|
|
# ${CLIENT_NAME} server - behind edge proxy (private network only)
|
|
|
|
|
|
|
|
|
|
# SSH via edge server as bastion/jump host
|
|
|
|
|
ansible_host: ${PRIVATE_IP}
|
|
|
|
|
ansible_ssh_common_args: '-o ProxyCommand="ssh -i ../keys/ssh/edge -W %h:%p -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@78.47.191.38" -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
|
|
|
|
|
|
|
|
|
|
# Client identification
|
|
|
|
|
client_name: ${CLIENT_NAME}
|
|
|
|
|
client_domain: ${CLIENT_NAME}.vrije.cloud
|
|
|
|
|
client_secrets_file: ${CLIENT_NAME}.sops.yaml
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
echo -e "${GREEN}✓ Created host_vars file: $HOST_VARS_FILE${NC}"
|
|
|
|
|
echo ""
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-17 21:34:05 +01:00
|
|
|
echo "Configuration added:"
|
|
|
|
|
echo " Server: $SERVER_TYPE in $LOCATION"
|
|
|
|
|
echo " Volume: $VOLUME_SIZE GB"
|
|
|
|
|
echo " Apps: $APPS"
|
chore: Post-workshop state - January 23rd, 2026
This commit captures the infrastructure state immediately following
the "Post-Tyranny Tech" workshop on January 23rd, 2026.
Infrastructure Status:
- 13 client servers deployed (white, valk, zwaan, specht, das, uil, vos,
haas, wolf, ree, mees, mus, mol, kikker)
- Services: Authentik SSO, Nextcloud, Collabora Office, Traefik
- Private network architecture with edge NAT gateway
- OIDC integration between Authentik and Nextcloud
- Automated recovery flows and invitation system
- Container update monitoring with Diun
- Uptime monitoring with Uptime Kuma
Changes include:
- Multiple new client host configurations
- Network architecture improvements (private IPs + NAT)
- DNS management automation
- Container update notifications
- Email configuration via Mailgun
- SSH key generation for all clients
- Encrypted secrets for all deployments
- Health check and diagnostic scripts
Known Issues to Address:
- Nextcloud version pinned to v30 (should use 'latest' or v32)
- Zitadel references in templates (migrated to Authentik but templates not updated)
- Traefik dynamic config has obsolete static routes
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 20:36:31 +01:00
|
|
|
echo " Private IP: $PRIVATE_IP"
|
2026-01-17 21:34:05 +01:00
|
|
|
echo ""
|
|
|
|
|
echo -e "${CYAN}Next steps:${NC}"
|
|
|
|
|
echo "1. Review changes: cat tofu/terraform.tfvars"
|
|
|
|
|
echo "2. Plan infrastructure: cd tofu && tofu plan"
|
|
|
|
|
echo "3. Apply infrastructure: cd tofu && tofu apply"
|
|
|
|
|
echo "4. Deploy services: ./scripts/deploy-client.sh $CLIENT_NAME"
|
|
|
|
|
echo ""
|