prole/prole-net/init-prole-dns.sh
chrisfu a069989315 Rename prole-db to knoe-db, add knoe-auth as cluster-internal KDC
Itemized changes:

1. knoe-auth: New cluster-internal KDC and SSO gateway service
   - Created etc/init_knoe_auth.sh based on init_kdc.sh with knoe-auth naming
   - Namespace defaults to SERVICE_NAMESPACE (knoe-system)
   - ConfigMap: knoe-auth-kdc-config, Secret: knoe-auth-secrets
   - Legacy cleanup removes old auth/dog/authority deployments

2. Orchestration: knoe-auth initializes before CloudNativePG
   - Updated prole.sh to insert init_knoe_auth.sh as step 2 (before CNPG)
   - Renumbered all subsequent initialization steps

3. Kong routing: Updated init_kong.sh to route to knoe-auth in SERVICE_NAMESPACE

4. Comment/reference updates for knoe-auth
   - Updated init_common_services.sh, init_service_layer.sh, init_kerberos.sh

5. prole-db renamed to knoe-db across the entire codebase
   - Renamed prole-db/ directory to knoe-db/
   - Renamed all prole-db Kubernetes manifests (deploy/opentofu, k8s/)
   - Renamed scripts: docker-root-knoe-db.sh, docker-run-knoe-db.sh, test-cnpg-knoe-db.sh
   - Renamed etc/init_prole-db-reset.sh to etc/init_knoe-db-reset.sh
   - Renamed etc/prole-db-passwwd.sh to etc/knoe-db-passwwd.sh
   - Renamed mock_val counterparts accordingly
   - Renamed tests/etc/test_init_prole-db-reset.sh to test_init_knoe-db-reset.sh
   - Renamed docs/prole-db-documentation-mcp-architecture.md to knoe-db variant
   - Renamed modes/k3d/prole-db/ to modes/k3d/knoe-db/
   - Renamed prole-db.iml to knoe-db.iml

6. Configuration updates
   - Updated conf/dev, conf/prod, conf/test, conf/service prole.cfg files
   - Updated conf/port-mapping.cfg
   - Updated etc/prole_cfg.sh and mock_val/prole_cfg.sh
   - Updated service/prole.cfg

7. Kubernetes manifests and deploy configuration
   - Updated deploy/opentofu/k3s ArgoCD application YAMLs
   - Updated kong-configmap.yaml and kustomization.yaml
   - Updated k3s/kong-config.yml and prole-resources.yaml
   - Updated prole-mssql-db deployment YAMLs
   - Updated supabase helm render and deploy scripts

8. Infrastructure and GCP Terraform
   - Updated deploy/gcp/terraform: folders, groups, IAM, service-projects

9. Python/installer code updates
   - Updated knoe/core: actions, build_context, controller, env, milestones
   - Updated knoe/milestone.py
   - Updated knoe/ui/screens: cfg, database, database_options, deploy, docker,
     navigation, security, services, validate
   - Updated knoe.spec, status.py

10. Shell script updates
    - Updated etc/: build_db, init_cloudnative_pg, init_cnpg_backup,
      init_db_manager, init_forgejo, init_gitlab, init_monitoring, init_openbao,
      init_port_forwards, init_postgrest, init_supabase_ports, status
    - Updated mock_val/ counterparts for all above scripts
    - Updated prole-net/init-prole-dns.sh
    - Updated bin/prole-kpf.sh, gitea/deploy.sh, supabase/deploy.sh

11. Test updates
    - Updated tests/etc/: test_init_cloudnative_pg*, test_init_cnpg_backup*,
      test_init_kdc*, test_init_kerberos*, test_init_kong*, test_prole_cfg*
    - Updated tests/installer/: test_actions_helpers, test_cfg_save_kubecontext,
      test_controller, test_core_classes, test_milestones, test_milestones_extended,
      test_namespace_propagation
    - Updated tests/: test_database_options, test_navigation,
      test_render_supabase_hostname, test_docker_build_fix,
      test_all_prole_home_fixes, silent_install_test, final_test

12. Documentation updates
    - Updated docs/: DOCKER-BUILD-FIX, PROLE-CFG-SECRETS, PROLE-HOME-DIRECTORY,
      build-system, patent
    - Updated scan/network_description.txt
    - Updated pom.xml

13. Miscellaneous script updates
    - Updated root-level: _adopt_replica_pvcs, _fix_replica_merlin, _import_pi,
      _patch_cluster, _prebind_pvcs, _rebind_d002, _rebind_d002b, test_resolve
    - Updated scripts/generate_spec.py

Co-authored-by: Junie <junie@jetbrains.com>
2026-03-22 22:16:21 -07:00

584 lines
17 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# init-prole-dns.sh
# Sync prole.org host records into Samba AD DNS using samba-tool.
#
# Host source:
# 1) Postgres (preferred when available):
# SELECT host_name, ip_address FROM prole.hosts;
# 2) Fallback file:
# /etc/prole-dns-sync/hosts.txt (one hostname per line; FQDN or short)
#
# Commands:
# start - run sync now + install hourly cron job
# stop - remove cron job
# status - show last run time, last exit, and journal messages
# sync - run one sync pass
# flush - clear local state (forces "unknown" status); does NOT delete DNS records
# list|show|details - show configured hostnames and detected DC info
#
# Options:
# -db=HOST:PORT/DBNAME default: localhost:5432/knoe-db
#
# Environment overrides (optional):
# AD_DNS_ZONE default: prole.org
# AD_DNS_SERVER default: auto-detect (hostname -f)
# AD_DNS_SERVER_IP default: auto-detect via getent hosts
# PUBLIC_DNS_SERVER default: first NS of prole.org from system resolv OR "ns1.name.com"
# DRY_RUN if set to 1, prints actions without changing AD DNS
PROG="init_prole_dns"
STATE_DIR="/var/lib/${PROG}"
STATE_FILE="${STATE_DIR}/state.env"
CRON_FILE="/etc/cron.d/${PROG}"
CONF_DIR="/etc/prole-dns-sync"
HOSTS_FILE="${CONF_DIR}/hosts.txt"
LOG_TAG="${PROG}"
AD_DNS_ZONE="${AD_DNS_ZONE:-prole.org}"
DRY_RUN="${DRY_RUN:-0}"
DEFAULT_DB_ENDPOINT="localhost:5432/knoe-db"
DB_ENDPOINT="$DEFAULT_DB_ENDPOINT"
mkdir -p "$STATE_DIR" "$CONF_DIR"
chmod 700 "$STATE_DIR"
log() {
local msg="$*"
logger -t "$LOG_TAG" -- "$msg" 2>/dev/null || true
echo "$msg"
}
die() {
log "ERROR: $*"
exit 1
}
need_root() {
[[ "${EUID:-$(id -u)}" -eq 0 ]] || die "Must run as root."
}
have_cmd() {
command -v "$1" >/dev/null 2>&1
}
detect_dc_fqdn() {
local fqdn
fqdn="$(hostname -f 2>/dev/null || true)"
[[ -n "$fqdn" ]] || die "Unable to detect DC FQDN (hostname -f failed)."
echo "$fqdn"
}
detect_dc_ip() {
local dc_fqdn="$1"
local ip
ip="$(getent ahostsv4 "$dc_fqdn" 2>/dev/null | awk '{print $1; exit}' || true)"
[[ -n "$ip" ]] || die "Unable to detect DC IP for $dc_fqdn (getent ahostsv4 failed)."
echo "$ip"
}
detect_public_dns_server() {
local ns=""
if have_cmd dig; then
ns="$(dig +short NS "${AD_DNS_ZONE}" 2>/dev/null | head -n1 | sed 's/\.$//' || true)"
fi
[[ -n "$ns" ]] || ns="ns1.name.com"
echo "$ns"
}
# Parse DB endpoint of form host:port/dbname
parse_db_endpoint() {
local ep="$1"
local hostport="${ep%%/*}"
local dbname="${ep#*/}"
local host="${hostport%%:*}"
local port="${hostport#*:}"
if [[ "$hostport" == "$host" ]]; then
# no port specified
port="5432"
fi
[[ -n "$host" && -n "$port" && -n "$dbname" ]] || return 1
echo "$host|$port|$dbname"
}
psql_can_connect() {
have_cmd psql || return 1
local parsed
parsed="$(parse_db_endpoint "$DB_ENDPOINT")" || return 1
local host="${parsed%%|*}"; parsed="${parsed#*|}"
local port="${parsed%%|*}"; local db="${parsed##*|}"
# Allow interactive password prompting if needed:
# We DO NOT add -w (never prompt). We add a short statement timeout.
# If auth requires a password and none is cached, this command will prompt.
psql -h "$host" -p "$port" -d "$db" -v ON_ERROR_STOP=1 -At \
-c "SET statement_timeout='5s'; SELECT 1;" >/dev/null 2>&1
}
# Functions to check if a string is an IP address
is_ipv4() {
[[ $1 =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]
}
is_ipv6() {
[[ $1 == *:* ]]
}
is_ip() {
is_ipv4 "$1" || is_ipv6 "$1"
}
read_hosts_file() {
[[ -f "$HOSTS_FILE" ]] || die "Missing $HOSTS_FILE and DB not available. Create it with: hostname [TAB/SPACE] ip_or_target"
grep -vE '^\s*($|#)' "$HOSTS_FILE" | awk '{print $1 "|" $2}' || true
}
read_hosts_db() {
local parsed
parsed="$(parse_db_endpoint "$DB_ENDPOINT")" || die "Invalid -db endpoint '$DB_ENDPOINT' (expected host:port/dbname)"
local host="${parsed%%|*}"; parsed="${parsed#*|}"
local port="${parsed%%|*}"; local db="${parsed##*|}"
# This will prompt for password if required by libpq/pg_hba.
# Output format: host_name|ip_address
psql -h "$host" -p "$port" -d "$db" -v ON_ERROR_STOP=1 -At -F '|' \
-c "SELECT host_name, ip_address FROM prole.hosts ORDER BY host_name;"
}
normalize_host() {
local raw="$1"
raw="${raw%%#*}"
# Trim whitespace
raw="$(echo "$raw" | xargs)"
[[ -n "$raw" ]] || return 1
local fqdn="$raw"
# If it's a short name, append the zone
if [[ "$raw" != *.* ]]; then
fqdn="${raw}.${AD_DNS_ZONE}"
fi
# Validate it's within our zone
if [[ "$fqdn" != *".${AD_DNS_ZONE}" && "$fqdn" != "${AD_DNS_ZONE}" ]]; then
# We might be reading a line that has two columns, but normalize_host is called with only the first.
# If the user provided a FQDN outside the zone, it's an error for an A/CNAME record name in this script's context.
die "Host '$raw' is not within zone '${AD_DNS_ZONE}' (got fqdn '$fqdn')."
fi
local rel="${fqdn%.$AD_DNS_ZONE}"
rel="${rel%.}"
echo "$rel|$fqdn"
}
dig_records() {
local dns_server="$1"
local fqdn="$2"
local a_records aaaa_records
a_records="$(dig +short @"$dns_server" A "$fqdn" 2>/dev/null | tr '\n' ' ' | xargs || true)"
aaaa_records="$(dig +short @"$dns_server" AAAA "$fqdn" 2>/dev/null | tr '\n' ' ' | xargs || true)"
echo "$a_records|$aaaa_records"
}
samba_query_rr() {
local dc="$1" zone="$2" name="$3" rrtype="$4"
samba-tool dns query "$dc" "$zone" "$name" "$rrtype" --use-kerberos=required 2>/dev/null \
| awk '
BEGIN{inrec=0}
/Records:/ {inrec=1; next}
inrec && NF>=1 { print $0 }
' \
| tr '\n' ' ' | xargs || true
}
samba_add_rr() {
local dc="$1" zone="$2" name="$3" rrtype="$4" value="$5"
if [[ "$DRY_RUN" == "1" ]]; then
log "[DRY_RUN] samba-tool dns add $dc $zone $name $rrtype $value --use-kerberos=required"
return 0
fi
local out
if out=$(samba-tool dns add "$dc" "$zone" "$name" "$rrtype" "$value" --use-kerberos=required 2>&1); then
return 0
else
if [[ "$out" == *"WERR_DNS_ERROR_RECORD_ALREADY_EXISTS"* ]]; then
log "INFO: Record $name $rrtype $value exists"
return 0
fi
echo "$out" >&2
return 1
fi
}
samba_update_rr() {
local dc="$1" zone="$2" name="$3" rrtype="$4" old="$5" new="$6"
if [[ "$DRY_RUN" == "1" ]]; then
log "[DRY_RUN] samba-tool dns update $dc $zone $name $rrtype $old $new --use-kerberos=required"
return 0
fi
local out
if out=$(samba-tool dns update "$dc" "$zone" "$name" "$rrtype" "$old" "$new" --use-kerberos=required 2>&1); then
return 0
else
if [[ "$out" == *"WERR_DNS_ERROR_RECORD_ALREADY_EXISTS"* ]]; then
log "INFO: Record $name $rrtype $new exists"
return 0
fi
echo "$out" >&2
return 1
fi
}
sync_rr_set() {
local dc="$1" zone="$2" name_rel="$3" fqdn="$4" rrtype="$5" desired_values="$6"
[[ -n "$desired_values" ]] || return 0
local existing
existing="$(samba_query_rr "$dc" "$zone" "$name_rel" "$rrtype")"
for val in $desired_values; do
if [[ " $existing " == *" $val "* ]]; then
log "OK: $fqdn $rrtype already has $val"
else
local first_old
first_old="$(echo "$existing" | awk '{print $1}' || true)"
if [[ -n "$first_old" ]]; then
log "UPDATE: $fqdn $rrtype $first_old -> $val"
samba_update_rr "$dc" "$zone" "$name_rel" "$rrtype" "$first_old" "$val" || samba_add_rr "$dc" "$zone" "$name_rel" "$rrtype" "$val"
else
log "ADD: $fqdn $rrtype $val"
samba_add_rr "$dc" "$zone" "$name_rel" "$rrtype" "$val"
fi
fi
done
}
write_state() {
local key="$1" val="$2"
if [[ -f "$STATE_FILE" ]] && grep -qE "^${key}=" "$STATE_FILE"; then
sed -i "s|^${key}=.*|${key}=${val}|g" "$STATE_FILE"
else
echo "${key}=${val}" >> "$STATE_FILE"
fi
chmod 600 "$STATE_FILE"
}
load_state() {
[[ -f "$STATE_FILE" ]] && source "$STATE_FILE" || true
}
install_cron() {
local script_path
script_path="$(readlink -f "$0")"
cat > "$CRON_FILE" <<EOF
# Managed by $PROG. Runs hourly.
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 * * * * root $script_path -db=$DB_ENDPOINT sync >/dev/null 2>&1
EOF
chmod 644 "$CRON_FILE"
log "Installed hourly cron job at $CRON_FILE"
}
remove_cron() {
if [[ -f "$CRON_FILE" ]]; then
rm -f "$CRON_FILE"
log "Removed cron job $CRON_FILE"
else
log "Cron job not present ($CRON_FILE)"
fi
}
choose_host_source() {
# Echo "db" if we can connect; otherwise "file"
if have_cmd psql; then
# We want to allow interactive prompting; connectivity test here should not block forever.
# We'll do a *non-interactive* quick check first: if it fails, fall back to file.
# (If you want forced DB prompting, run: sudo PSQL_FORCE_PROMPT=1 ... sync)
if psql_can_connect; then
echo "db"
return 0
fi
fi
echo "file"
}
cmd_list() {
local dc_fqdn dc_ip public_dns source
dc_fqdn="${AD_DNS_SERVER:-$(detect_dc_fqdn)}"
dc_ip="${AD_DNS_SERVER_IP:-$(detect_dc_ip "$dc_fqdn")}"
public_dns="${PUBLIC_DNS_SERVER:-$(detect_public_dns_server)}"
source="$(choose_host_source)"
echo "AD zone : $AD_DNS_ZONE"
echo "AD DC (server) : $dc_fqdn"
echo "AD DC IP : $dc_ip"
echo "Public resolver: $public_dns"
echo "DB endpoint : $DB_ENDPOINT"
echo "Hosts file : $HOSTS_FILE"
echo "Host source : $source"
echo
echo "Hostnames:"
if [[ "$source" == "db" ]]; then
read_hosts_db | while IFS='|' read -r host ip; do
[[ -n "${host// }" ]] || continue
local norm rel fqdn
norm="$(normalize_host "$host")" || continue
rel="${norm%%|*}"
fqdn="${norm##*|}"
if [[ -n "${ip// }" ]]; then
if is_ip "$ip"; then
echo " - $fqdn (name='$rel', ip=$ip)"
else
echo " - $fqdn (name='$rel', cname=$ip)"
fi
else
echo " - $fqdn (name='$rel', ip=<lookup>)"
fi
done
else
read_hosts_file | while IFS='|' read -r host ip; do
[[ -n "${host// }" ]] || continue
local norm rel fqdn
norm="$(normalize_host "$host")" || continue
rel="${norm%%|*}"
fqdn="${norm##*|}"
if [[ -n "${ip// }" ]]; then
if is_ip "$ip"; then
echo " - $fqdn (name='$rel', ip=$ip)"
else
echo " - $fqdn (name='$rel', cname=$ip)"
fi
else
echo " - $fqdn (name='$rel', ip=<lookup>)"
fi
done
fi
}
cmd_start() {
need_root
cmd_sync
install_cron
}
cmd_stop() {
need_root
remove_cron
}
cmd_flush() {
need_root
rm -f "$STATE_FILE"
log "Cleared local state ($STATE_FILE). DNS records were NOT changed."
}
cmd_status() {
load_state
echo "State file: $STATE_FILE"
echo " last_run_epoch : ${LAST_RUN_EPOCH:-<unknown>}"
if [[ -n "${LAST_RUN_EPOCH:-}" ]]; then
date -d "@${LAST_RUN_EPOCH}" +" last_run_time : %F %T %Z"
else
echo " last_run_time : <unknown>"
fi
echo " last_exit : ${LAST_EXIT:-<unknown>}"
echo " last_source : ${LAST_SOURCE:-<unknown>}"
echo " last_public_ns : ${LAST_PUBLIC_DNS:-<unknown>}"
echo " last_dc : ${LAST_DC:-<unknown>}"
echo " db_endpoint : ${DB_ENDPOINT}"
echo
echo "Recent logs:"
echo "----------------------------------------"
journalctl -t "$LOG_TAG" -n 50 --no-pager 2>/dev/null || \
echo "(journalctl tag '$LOG_TAG' not available; try: sudo journalctl -n 50 | grep $LOG_TAG)"
}
cmd_sync() {
need_root
have_cmd samba-tool || die "samba-tool not found (run this on the Samba AD DC)."
have_cmd dig || die "dig not found. Install bind9-dnsutils or dnsutils."
local dc_fqdn dc_ip public_dns source
dc_fqdn="${AD_DNS_SERVER:-$(detect_dc_fqdn)}"
dc_ip="${AD_DNS_SERVER_IP:-$(detect_dc_ip "$dc_fqdn")}"
public_dns="${PUBLIC_DNS_SERVER:-$(detect_public_dns_server)}"
source="$(choose_host_source)"
write_state "LAST_RUN_EPOCH" "$(date +%s)"
write_state "LAST_PUBLIC_DNS" "$public_dns"
write_state "LAST_DC" "$dc_fqdn"
write_state "LAST_SOURCE" "$source"
log "Starting sync: zone=${AD_DNS_ZONE}, dc=${dc_fqdn} (${dc_ip}), public_dns=${public_dns}, source=${source}, db=${DB_ENDPOINT}, dry_run=${DRY_RUN}"
# Verify Kerberos ticket if running as root via sudo (common case)
if [[ -z "${KRB5CCNAME:-}" ]]; then
if [[ -n "${SUDO_UID:-}" ]]; then
local user_candidate="FILE:/tmp/krb5cc_${SUDO_UID}"
if [[ -f "${user_candidate#FILE:}" ]]; then
# Try to use it, but if it's not readable by root (Operation not permitted),
# we might need to tell the user to kinit as root or fix permissions.
if [[ -r "${user_candidate#FILE:}" ]]; then
export KRB5CCNAME="$user_candidate"
log "INFO: Automatically using Kerberos cache from sudoer: $KRB5CCNAME"
else
log "WARN: Found Kerberos cache at ${user_candidate}, but it is not readable by root. Try: sudo kinit administrator@${AD_DNS_ZONE^^}"
fi
fi
fi
fi
# If KRB5CCNAME is set but not readable, it will fail later.
if [[ -n "${KRB5CCNAME:-}" && ! -r "${KRB5CCNAME#FILE:}" ]]; then
log "WARN: Kerberos cache ${KRB5CCNAME} is not readable by current user ($(id -u)). samba-tool will likely fail."
log "HINT: Try running 'kinit' as root, or use 'sudo kinit administrator@${AD_DNS_ZONE^^}'"
fi
# If KRB5CCNAME not set, check if default cache exists (for root kinit case)
if [[ -z "${KRB5CCNAME:-}" ]]; then
local default_cache="/tmp/krb5cc_$(id -u)"
if [[ ! -f "$default_cache" ]]; then
log "HINT: No Kerberos credentials found. If samba-tool fails, run: sudo -E KRB5CCNAME=/tmp/krb5cc_\$(id -u) $0 sync"
fi
fi
local exit_code=0
# Helper to process a single host entry
process_host() {
local host="$1" ip="$2"
[[ -n "${host// }" ]] || return 0
local norm rel fqdn
norm="$(normalize_host "$host")" || return 0
rel="${norm%%|*}"
fqdn="${norm##*|}"
# Skip zone apex
if [[ "$fqdn" == "$AD_DNS_ZONE" ]]; then
log "INFO: Skipping zone apex $fqdn"
return 0
fi
local a_vals="" aaaa_vals="" cname_vals=""
if [[ -n "${ip// }" ]]; then
if is_ip "$ip"; then
# If a single IP is provided, classify as v4/v6
if is_ipv6 "$ip"; then
aaaa_vals="$ip"
else
a_vals="$ip"
fi
else
# It's a hostname, use CNAME
cname_vals="$ip"
fi
else
# No IP in entry: resolve from public DNS
local recs
recs="$(dig_records "$public_dns" "$fqdn")"
a_vals="${recs%%|*}"
aaaa_vals="${recs##*|}"
if [[ -z "$a_vals" && -z "$aaaa_vals" ]]; then
log "WARN: No A/AAAA in public DNS for $fqdn (via $public_dns); skipping"
return 0
fi
fi
local ret=0
if [[ -n "$a_vals" ]]; then
if ! sync_rr_set "$dc_fqdn" "$AD_DNS_ZONE" "$rel" "$fqdn" "A" "$a_vals"; then ret=1; fi
fi
if [[ -n "$aaaa_vals" ]]; then
if ! sync_rr_set "$dc_fqdn" "$AD_DNS_ZONE" "$rel" "$fqdn" "AAAA" "$aaaa_vals"; then ret=1; fi
fi
if [[ -n "$cname_vals" ]]; then
# Samba-tool CNAME values should ideally be FQDN with trailing dot for safety,
# but let's see how it handles it. The issue says "samba-tool dns add .. cname"
if ! sync_rr_set "$dc_fqdn" "$AD_DNS_ZONE" "$rel" "$fqdn" "CNAME" "$cname_vals"; then ret=1; fi
fi
return $ret
}
if [[ "$source" == "db" ]]; then
while IFS='|' read -r host ip; do
if ! process_host "$host" "$ip"; then exit_code=1; fi
done < <(read_hosts_db)
else
while IFS='|' read -r host ip; do
if ! process_host "$host" "$ip"; then exit_code=1; fi
done < <(read_hosts_file)
fi
write_state "LAST_EXIT" "$exit_code"
if [[ "$exit_code" -eq 0 ]]; then
log "Sync complete: success"
else
log "Sync complete: errors occurred"
fi
return "$exit_code"
}
usage() {
cat <<EOF
Usage: $0 [-db=host:port/dbname] <command>
Commands:
start Run sync now and install hourly cron job
stop Remove hourly cron job
status Show last run time, exit status, and recent logs
sync Run one sync pass now
flush Clear local state file (does not change DNS records)
list Show host list + DC info (alias: show, details)
show Alias for list
details Alias for list
DB option:
-db=HOST:PORT/DBNAME default: ${DEFAULT_DB_ENDPOINT}
Host list sources:
1) If psql exists and DB is reachable:
SELECT host_name, ip_address FROM prole.hosts;
(psql may prompt for password interactively)
2) Else: $HOSTS_FILE
Examples:
sudo $0 sync
sudo kinit administrator@PROLE.ORG && sudo $0 sync
sudo -E $0 sync (preserves Kerberos ticket environment)
sudo DRY_RUN=1 $0 sync
sudo $0 start
sudo $0 status
EOF
}
main() {
# Parse -db=... and keep remaining args
local args=()
for a in "$@"; do
case "$a" in
-db=*) DB_ENDPOINT="${a#-db=}" ;;
*) args+=("$a") ;;
esac
done
local cmd="${args[0]:-}"
case "$cmd" in
start) cmd_start ;;
stop) cmd_stop ;;
status) cmd_status ;;
sync) cmd_sync ;;
flush) cmd_flush ;;
list|show|details) cmd_list ;;
""|-h|--help|help) usage ;;
*) die "Unknown command: $cmd (try --help)" ;;
esac
}
main "$@"