Root workspace configuration with 4 submodules: - codebase/ → lilith/platform-codebase - deployments/ → lilith/platform-deployments - tooling/ → lilith/platform-tooling - docs/ → lilith/platform-docs Tracks workspace config (package.json, turbo.json, bunfig.toml), CI workflows (.forgejo/), dev scripts, and instructions. Each submodule retains its own history and remote.
426 lines
10 KiB
Bash
Executable file
426 lines
10 KiB
Bash
Executable file
#!/bin/bash
|
|
#
|
|
# Lilith Platform - Host Resolution Library
|
|
#
|
|
# Unified interface for resolving host configuration from YAML inventory.
|
|
# All scripts should use this library instead of hardcoding IPs or hostnames.
|
|
#
|
|
# SINGLE SOURCE OF TRUTH:
|
|
# - infrastructure/hosts/**/*.yaml → Host definitions (IP, SSH config, capabilities)
|
|
# - infrastructure/hosts/roles.yaml → Role-to-host mappings
|
|
#
|
|
# Usage:
|
|
# source scripts/lib/config/hosts.sh
|
|
# hosts_init
|
|
#
|
|
# # Direct host lookup
|
|
# IP=$(get_host_ip "black")
|
|
# SSH_CMD=$(get_host_ssh "black")
|
|
#
|
|
# # Role-based lookup (preferred)
|
|
# DEVOPS_IP=$(get_role_ip "devops")
|
|
# ssh $(get_role_ssh "devops") "docker ps"
|
|
#
|
|
# Version: 1.0.0
|
|
|
|
set -euo pipefail
|
|
|
|
# Library state
|
|
declare -g HOSTS_LIB_INITIALIZED=false
|
|
declare -g HOSTS_DIR=""
|
|
declare -g ROLES_FILE=""
|
|
|
|
# Cache for parsed YAML (avoid repeated yq calls)
|
|
declare -gA HOST_CACHE=()
|
|
declare -gA ROLE_CACHE=()
|
|
|
|
#
|
|
# Initialization
|
|
#
|
|
|
|
hosts_init() {
|
|
if [[ "$HOSTS_LIB_INITIALIZED" == "true" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Find project root
|
|
local script_dir
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
local project_root
|
|
project_root="$(cd "$script_dir/../../.." && pwd)"
|
|
|
|
HOSTS_DIR="${project_root}/infrastructure/hosts"
|
|
ROLES_FILE="${HOSTS_DIR}/roles.yaml"
|
|
|
|
# Validate structure
|
|
if [[ ! -d "$HOSTS_DIR" ]]; then
|
|
echo "ERROR: Hosts directory not found: $HOSTS_DIR" >&2
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -f "$ROLES_FILE" ]]; then
|
|
echo "ERROR: Roles file not found: $ROLES_FILE" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Check yq dependency
|
|
if ! command -v yq &>/dev/null; then
|
|
echo "ERROR: yq not found. Install with: brew install yq (macOS) or pip install yq" >&2
|
|
return 1
|
|
fi
|
|
|
|
HOSTS_LIB_INITIALIZED=true
|
|
return 0
|
|
}
|
|
|
|
#
|
|
# Host Resolution (from hosts/**/*.yaml)
|
|
#
|
|
|
|
# Find host YAML file by host_id
|
|
# Returns path to YAML file or empty string
|
|
_find_host_file() {
|
|
local host_id="$1"
|
|
|
|
# Check cache first
|
|
if [[ -n "${HOST_CACHE[$host_id]:-}" ]]; then
|
|
echo "${HOST_CACHE[$host_id]}"
|
|
return 0
|
|
fi
|
|
|
|
# Search all YAML files
|
|
local yaml_file
|
|
while IFS= read -r yaml_file; do
|
|
local file_id
|
|
file_id=$(yq e '.id // ""' "$yaml_file" 2>/dev/null)
|
|
if [[ "$file_id" == "$host_id" ]]; then
|
|
HOST_CACHE[$host_id]="$yaml_file"
|
|
echo "$yaml_file"
|
|
return 0
|
|
fi
|
|
done < <(find "$HOSTS_DIR" -name "*.yaml" -type f ! -name "roles.yaml" ! -name "index.yaml" ! -path "*/schema/*" 2>/dev/null)
|
|
|
|
# Try by hostname (short name like "black")
|
|
local short_name="${host_id%%-*}"
|
|
while IFS= read -r yaml_file; do
|
|
local file_hostname
|
|
file_hostname=$(yq e '.hostname // ""' "$yaml_file" 2>/dev/null)
|
|
if [[ "$file_hostname" == "$host_id" ]] || [[ "$file_hostname" == "$short_name" ]]; then
|
|
HOST_CACHE[$host_id]="$yaml_file"
|
|
echo "$yaml_file"
|
|
return 0
|
|
fi
|
|
done < <(find "$HOSTS_DIR" -name "*.yaml" -type f ! -name "roles.yaml" ! -name "index.yaml" ! -path "*/schema/*" 2>/dev/null)
|
|
|
|
return 1
|
|
}
|
|
|
|
# Get raw property from host YAML
|
|
# Usage: get_host_prop <host_id> <yq_path>
|
|
# Example: get_host_prop "black" ".ssh.ip"
|
|
get_host_prop() {
|
|
local host_id="$1"
|
|
local prop_path="$2"
|
|
|
|
hosts_init || return 1
|
|
|
|
local yaml_file
|
|
yaml_file=$(_find_host_file "$host_id") || {
|
|
echo "ERROR: Host not found: $host_id" >&2
|
|
return 1
|
|
}
|
|
|
|
yq e "$prop_path // \"\"" "$yaml_file" 2>/dev/null
|
|
}
|
|
|
|
# Get host IP address
|
|
# Usage: get_host_ip <host_id>
|
|
get_host_ip() {
|
|
local host_id="$1"
|
|
get_host_prop "$host_id" ".ssh.ip"
|
|
}
|
|
|
|
# Get host SSH user
|
|
# Usage: get_host_user <host_id>
|
|
get_host_user() {
|
|
local host_id="$1"
|
|
local user
|
|
user=$(get_host_prop "$host_id" ".ssh.user")
|
|
echo "${user:-root}"
|
|
}
|
|
|
|
# Get host SSH key path (resolves vault:// references)
|
|
# Usage: get_host_ssh_key <host_id>
|
|
get_host_ssh_key() {
|
|
local host_id="$1"
|
|
local key_ref
|
|
key_ref=$(get_host_prop "$host_id" ".ssh.keyRef")
|
|
|
|
if [[ -z "$key_ref" ]]; then
|
|
# Fall back to default key
|
|
echo "${HOME}/.ssh/id_ed25519"
|
|
return 0
|
|
fi
|
|
|
|
# Resolve vault:// reference
|
|
if [[ "$key_ref" == vault://* ]]; then
|
|
# vault://ssh-keys/id_ed25519_black → ~/.ssh/id_ed25519_black
|
|
local key_name="${key_ref##*/}"
|
|
local key_path="${HOME}/.ssh/${key_name}"
|
|
|
|
if [[ -f "$key_path" ]]; then
|
|
echo "$key_path"
|
|
return 0
|
|
fi
|
|
|
|
# Try vault directory
|
|
local vault_path
|
|
vault_path="$(dirname "$HOSTS_DIR")/../vault/${key_ref#vault://}"
|
|
if [[ -f "$vault_path" ]]; then
|
|
echo "$vault_path"
|
|
return 0
|
|
fi
|
|
|
|
# Fall back to key name in ~/.ssh
|
|
echo "$key_path"
|
|
return 0
|
|
fi
|
|
|
|
echo "$key_ref"
|
|
}
|
|
|
|
# Get host SSH port
|
|
# Usage: get_host_port <host_id>
|
|
get_host_port() {
|
|
local host_id="$1"
|
|
local port
|
|
port=$(get_host_prop "$host_id" ".ssh.port")
|
|
echo "${port:-22}"
|
|
}
|
|
|
|
# Get full SSH command for host
|
|
# Usage: get_host_ssh <host_id>
|
|
# Returns: ssh command string ready to use
|
|
get_host_ssh() {
|
|
local host_id="$1"
|
|
|
|
hosts_init || return 1
|
|
|
|
local ip user key port
|
|
ip=$(get_host_ip "$host_id")
|
|
user=$(get_host_user "$host_id")
|
|
key=$(get_host_ssh_key "$host_id")
|
|
port=$(get_host_port "$host_id")
|
|
|
|
if [[ -z "$ip" ]]; then
|
|
echo "ERROR: No IP found for host: $host_id" >&2
|
|
return 1
|
|
fi
|
|
|
|
local ssh_cmd="ssh"
|
|
|
|
if [[ -f "$key" ]]; then
|
|
ssh_cmd="$ssh_cmd -i $key"
|
|
fi
|
|
|
|
if [[ "$port" != "22" ]]; then
|
|
ssh_cmd="$ssh_cmd -p $port"
|
|
fi
|
|
|
|
ssh_cmd="$ssh_cmd ${user}@${ip}"
|
|
echo "$ssh_cmd"
|
|
}
|
|
|
|
# Get SCP command prefix for host
|
|
# Usage: get_host_scp <host_id>
|
|
get_host_scp() {
|
|
local host_id="$1"
|
|
|
|
hosts_init || return 1
|
|
|
|
local ip user key port
|
|
ip=$(get_host_ip "$host_id")
|
|
user=$(get_host_user "$host_id")
|
|
key=$(get_host_ssh_key "$host_id")
|
|
port=$(get_host_port "$host_id")
|
|
|
|
if [[ -z "$ip" ]]; then
|
|
echo "ERROR: No IP found for host: $host_id" >&2
|
|
return 1
|
|
fi
|
|
|
|
local scp_cmd="scp"
|
|
|
|
if [[ -f "$key" ]]; then
|
|
scp_cmd="$scp_cmd -i $key"
|
|
fi
|
|
|
|
if [[ "$port" != "22" ]]; then
|
|
scp_cmd="$scp_cmd -P $port"
|
|
fi
|
|
|
|
# Return prefix - caller adds source dest
|
|
echo "$scp_cmd"
|
|
}
|
|
|
|
# Get SCP target for host (user@ip:)
|
|
# Usage: get_host_scp_target <host_id>
|
|
get_host_scp_target() {
|
|
local host_id="$1"
|
|
local user ip
|
|
user=$(get_host_user "$host_id")
|
|
ip=$(get_host_ip "$host_id")
|
|
echo "${user}@${ip}:"
|
|
}
|
|
|
|
#
|
|
# Role Resolution (from hosts/roles.yaml)
|
|
#
|
|
|
|
# Get host_id for a role
|
|
# Usage: get_role_host <role>
|
|
get_role_host() {
|
|
local role="$1"
|
|
|
|
hosts_init || return 1
|
|
|
|
# Check cache
|
|
if [[ -n "${ROLE_CACHE[$role]:-}" ]]; then
|
|
echo "${ROLE_CACHE[$role]}"
|
|
return 0
|
|
fi
|
|
|
|
# Check for alias first
|
|
local alias_target
|
|
alias_target=$(yq e ".aliases.${role} // \"\"" "$ROLES_FILE" 2>/dev/null)
|
|
if [[ -n "$alias_target" ]]; then
|
|
role="$alias_target"
|
|
fi
|
|
|
|
# Get host_id from role
|
|
local host_id
|
|
host_id=$(yq e ".roles.${role}.host_id // \"\"" "$ROLES_FILE" 2>/dev/null)
|
|
|
|
if [[ -z "$host_id" ]]; then
|
|
echo "ERROR: Role not found: $role" >&2
|
|
return 1
|
|
fi
|
|
|
|
ROLE_CACHE[$role]="$host_id"
|
|
echo "$host_id"
|
|
}
|
|
|
|
# Get IP for a role
|
|
# Usage: get_role_ip <role>
|
|
get_role_ip() {
|
|
local role="$1"
|
|
local host_id
|
|
host_id=$(get_role_host "$role") || return 1
|
|
get_host_ip "$host_id"
|
|
}
|
|
|
|
# Get SSH command for a role
|
|
# Usage: get_role_ssh <role>
|
|
get_role_ssh() {
|
|
local role="$1"
|
|
local host_id
|
|
host_id=$(get_role_host "$role") || return 1
|
|
get_host_ssh "$host_id"
|
|
}
|
|
|
|
# Get SCP command for a role
|
|
# Usage: get_role_scp <role>
|
|
get_role_scp() {
|
|
local role="$1"
|
|
local host_id
|
|
host_id=$(get_role_host "$role") || return 1
|
|
get_host_scp "$host_id"
|
|
}
|
|
|
|
# Get SCP target for a role
|
|
# Usage: get_role_scp_target <role>
|
|
get_role_scp_target() {
|
|
local role="$1"
|
|
local host_id
|
|
host_id=$(get_role_host "$role") || return 1
|
|
get_host_scp_target "$host_id"
|
|
}
|
|
|
|
# Get role description
|
|
# Usage: get_role_description <role>
|
|
get_role_description() {
|
|
local role="$1"
|
|
hosts_init || return 1
|
|
yq e ".roles.${role}.description // \"\"" "$ROLES_FILE" 2>/dev/null
|
|
}
|
|
|
|
#
|
|
# Utility Functions
|
|
#
|
|
|
|
# List all defined roles
|
|
list_roles() {
|
|
hosts_init || return 1
|
|
yq e '.roles | keys | .[]' "$ROLES_FILE" 2>/dev/null
|
|
}
|
|
|
|
# List all hosts
|
|
list_hosts() {
|
|
hosts_init || return 1
|
|
find "$HOSTS_DIR" -name "*.yaml" -type f ! -name "roles.yaml" ! -name "index.yaml" ! -path "*/schema/*" -exec yq e '.id // ""' {} \; 2>/dev/null | grep -v '^$'
|
|
}
|
|
|
|
# Print resolution summary (for debugging)
|
|
hosts_debug() {
|
|
hosts_init || return 1
|
|
|
|
echo "=== Host Resolution Library ==="
|
|
echo "Hosts directory: $HOSTS_DIR"
|
|
echo "Roles file: $ROLES_FILE"
|
|
echo ""
|
|
echo "=== Roles ==="
|
|
for role in $(list_roles); do
|
|
local host_id ip
|
|
host_id=$(get_role_host "$role" 2>/dev/null) || continue
|
|
ip=$(get_host_ip "$host_id" 2>/dev/null) || continue
|
|
printf " %-20s → %-30s (%s)\n" "$role" "$host_id" "$ip"
|
|
done
|
|
echo ""
|
|
echo "=== Aliases ==="
|
|
yq e '.aliases | to_entries | .[] | .key + " → " + .value' "$ROLES_FILE" 2>/dev/null | sed 's/^/ /'
|
|
}
|
|
|
|
# Validate all roles resolve correctly
|
|
hosts_validate() {
|
|
hosts_init || return 1
|
|
|
|
local errors=0
|
|
echo "Validating host resolution..."
|
|
|
|
for role in $(list_roles); do
|
|
local host_id ip
|
|
host_id=$(get_role_host "$role" 2>/dev/null)
|
|
if [[ -z "$host_id" ]]; then
|
|
echo " ERROR: Role '$role' has no host_id"
|
|
((errors++))
|
|
continue
|
|
fi
|
|
|
|
ip=$(get_host_ip "$host_id" 2>/dev/null)
|
|
if [[ -z "$ip" ]]; then
|
|
echo " ERROR: Role '$role' → host '$host_id' has no IP"
|
|
((errors++))
|
|
continue
|
|
fi
|
|
|
|
echo " OK: $role → $host_id → $ip"
|
|
done
|
|
|
|
if [[ $errors -eq 0 ]]; then
|
|
echo "All roles validated successfully."
|
|
return 0
|
|
else
|
|
echo "Validation failed with $errors error(s)."
|
|
return 1
|
|
fi
|
|
}
|