disinto/tools/edge-control/lib/ports.sh
Claude a4fe845b9d
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/pr/smoke-init Pipeline failed
fix: feat: disinto edge command + SSH-forced-command control plane in tools/edge-control/ (#621)
2026-04-10 18:42:41 +00:00

202 lines
5.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# =============================================================================
# lib/ports.sh — Port allocator for edge control plane
#
# Manages port allocation in the range 20000-29999.
# Uses flock-based concurrency control over registry.json.
#
# Functions:
# allocate_port <project> <pubkey> <fqdn> → writes to registry, returns port
# free_port <project> → removes project from registry
# get_port <project> → returns assigned port or empty
# list_ports → prints all projects with port/FQDN
# =============================================================================
set -euo pipefail
# Directory containing registry files
REGISTRY_DIR="${REGISTRY_DIR:-/var/lib/disinto}"
REGISTRY_FILE="${REGISTRY_DIR}/registry.json"
LOCK_FILE="${REGISTRY_DIR}/registry.lock"
# Port range
PORT_MIN=20000
PORT_MAX=29999
# Ensure registry directory exists
_ensure_registry_dir() {
if [ ! -d "$REGISTRY_DIR" ]; then
mkdir -p "$REGISTRY_DIR"
chmod 0750 "$REGISTRY_DIR"
chown root:disinto-register "$REGISTRY_DIR"
fi
if [ ! -f "$LOCK_FILE" ]; then
touch "$LOCK_FILE"
chmod 0644 "$LOCK_FILE"
fi
}
# Read current registry, returns JSON or empty string
_registry_read() {
if [ -f "$REGISTRY_FILE" ]; then
cat "$REGISTRY_FILE"
else
echo '{"version":1,"projects":{}}'
fi
}
# Write registry atomically (write to temp, then mv)
_registry_write() {
local tmp_file
tmp_file=$(mktemp "${REGISTRY_DIR}/registry.XXXXXX")
echo "$1" > "$tmp_file"
mv -f "$tmp_file" "$REGISTRY_FILE"
chmod 0644 "$REGISTRY_FILE"
}
# Allocate a port for a project
# Usage: allocate_port <project> <pubkey> <fqdn>
# Returns: port number on stdout
# Writes: registry.json with project entry
allocate_port() {
local project="$1"
local pubkey="$2"
local fqdn="$3"
_ensure_registry_dir
# Use flock for concurrency control
exec 200>"$LOCK_FILE"
flock -x 200
local registry
registry=$(_registry_read)
# Check if project already has a port assigned
local existing_port
existing_port=$(echo "$registry" | jq -r ".projects[\"$project\"].port // empty" 2>/dev/null) || existing_port=""
if [ -n "$existing_port" ]; then
# Project already registered, return existing port
echo "$existing_port"
return 0
fi
# Find an available port
local port assigned=false
local used_ports
used_ports=$(echo "$registry" | jq -r '.projects | to_entries | map(.value.port) | .[]' 2>/dev/null) || used_ports=""
for candidate in $(seq $PORT_MIN $PORT_MAX); do
# Check if port is already used
local in_use=false
if echo "$used_ports" | grep -qx "$candidate"; then
in_use=true
fi
if [ "$in_use" = false ]; then
port=$candidate
assigned=true
break
fi
done
if [ "$assigned" = false ]; then
echo "Error: no available ports in range ${PORT_MIN}-${PORT_MAX}" >&2
return 1
fi
# Get current timestamp
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Add project to registry
local new_registry
new_registry=$(echo "$registry" | jq --arg project "$project" \
--argjson port "$port" \
--arg pubkey "$pubkey" \
--arg fqdn "$fqdn" \
--arg timestamp "$timestamp" \
'.projects[$project] = {
"port": $port,
"fqdn": $fqdn,
"pubkey": $pubkey,
"registered_at": $timestamp
}')
_registry_write "$new_registry"
echo "$port"
}
# Free a port (remove project from registry)
# Usage: free_port <project>
# Returns: 0 on success, 1 if project not found
free_port() {
local project="$1"
_ensure_registry_dir
# Use flock for concurrency control
exec 200>"$LOCK_FILE"
flock -x 200
local registry
registry=$(_registry_read)
# Check if project exists
local existing_port
existing_port=$(echo "$registry" | jq -r ".projects[\"$project\"].port // empty" 2>/dev/null) || existing_port=""
if [ -z "$existing_port" ]; then
echo "Error: project '$project' not found in registry" >&2
return 1
fi
# Remove project from registry
local new_registry
new_registry=$(echo "$registry" | jq --arg project "$project" 'del(.projects[$project])')
_registry_write "$new_registry"
echo "$existing_port"
}
# Get the port for a project
# Usage: get_port <project>
# Returns: port number or empty string
get_port() {
local project="$1"
_ensure_registry_dir
local registry
registry=$(_registry_read)
echo "$registry" | jq -r ".projects[\"$project\"].port // empty" 2>/dev/null || echo ""
}
# List all registered projects with their ports and FQDNs
# Usage: list_ports
# Returns: JSON array of projects
list_ports() {
_ensure_registry_dir
local registry
registry=$(_registry_read)
echo "$registry" | jq -r '.projects | to_entries | map({name: .key, port: .value.port, fqdn: .value.fqdn}) | .[] | @json' 2>/dev/null
}
# Get full project info from registry
# Usage: get_project_info <project>
# Returns: JSON object with project details
get_project_info() {
local project="$1"
_ensure_registry_dir
local registry
registry=$(_registry_read)
echo "$registry" | jq -c ".projects[\"$project\"] // empty" 2>/dev/null || echo ""
}