Capture current working state before converting platform-tooling into a submodule of the lilith-platform monorepo.
1027 lines
29 KiB
Bash
Executable file
1027 lines
29 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# services.sh - Service configuration utilities for dev scripts
|
|
# =============================================================================
|
|
#
|
|
# Source this file in dev scripts to access service configuration from
|
|
# feature services.yaml files combined with ports.yaml
|
|
#
|
|
# Usage:
|
|
# source "$(dirname "$0")/lib/services.sh"
|
|
#
|
|
# # Get all dependencies for a feature
|
|
# get_feature_deps "analytics"
|
|
#
|
|
# # Get service URL
|
|
# get_service_url "analytics" "api"
|
|
#
|
|
# # Start all deps for a feature
|
|
# start_feature_deps "analytics"
|
|
#
|
|
# # Check health of all deps
|
|
# check_feature_health "analytics"
|
|
#
|
|
# =============================================================================
|
|
|
|
# Ensure ports.sh is loaded
|
|
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
if ! declare -f get_port &>/dev/null; then
|
|
source "$_SCRIPT_DIR/ports.sh"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# Configuration
|
|
# =============================================================================
|
|
|
|
SERVICES_PROJECT_ROOT="${SERVICES_PROJECT_ROOT:-$PORTS_PROJECT_ROOT}"
|
|
SERVICES_DIR="${SERVICES_PROJECT_ROOT}/codebase/features"
|
|
SERVICES_INFRA_DIR="${SERVICES_PROJECT_ROOT}/infrastructure/services/features"
|
|
|
|
# =============================================================================
|
|
# Service YAML Access
|
|
# =============================================================================
|
|
|
|
# Get the services.yaml path for a feature
|
|
# Usage: get_feature_services_yaml "analytics"
|
|
get_feature_services_yaml() {
|
|
local feature="$1"
|
|
local yaml_path="${SERVICES_DIR}/${feature}/services.yaml"
|
|
|
|
if [[ -f "$yaml_path" ]]; then
|
|
echo "$yaml_path"
|
|
return 0
|
|
fi
|
|
|
|
# Fallback to infrastructure symlink
|
|
yaml_path="${SERVICES_INFRA_DIR}/${feature}.yaml"
|
|
if [[ -f "$yaml_path" ]]; then
|
|
echo "$yaml_path"
|
|
return 0
|
|
fi
|
|
|
|
echo "ERROR: services.yaml not found for feature: $feature" >&2
|
|
return 1
|
|
}
|
|
|
|
# Check if a feature has a services.yaml
|
|
# Usage: has_services_yaml "analytics"
|
|
has_services_yaml() {
|
|
local feature="$1"
|
|
get_feature_services_yaml "$feature" &>/dev/null
|
|
}
|
|
|
|
# =============================================================================
|
|
# Service Access Functions
|
|
# =============================================================================
|
|
|
|
# Get list of services defined by a feature
|
|
# Usage: get_feature_services "analytics"
|
|
get_feature_services() {
|
|
_check_yq || return 1
|
|
local feature="$1"
|
|
local yaml_path
|
|
yaml_path=$(get_feature_services_yaml "$feature") || return 1
|
|
|
|
# Services are stored as arrays, extract the id field
|
|
yq '.services[].id' "$yaml_path" 2>/dev/null
|
|
}
|
|
|
|
# Get service type (api, postgresql, redis, frontend, etc.)
|
|
# Usage: get_service_type "analytics" "api"
|
|
get_service_type() {
|
|
_check_yq || return 1
|
|
local feature="$1"
|
|
local service="$2"
|
|
local yaml_path
|
|
yaml_path=$(get_feature_services_yaml "$feature") || return 1
|
|
|
|
# Services are arrays, find by id
|
|
yq ".services[] | select(.id == \"$service\") | .type // \"unknown\"" "$yaml_path" 2>/dev/null
|
|
}
|
|
|
|
# Get service port (resolved from ports.yaml)
|
|
# Usage: get_service_port "analytics" "api"
|
|
get_service_port() {
|
|
_check_yq || return 1
|
|
local feature="$1"
|
|
local service="$2"
|
|
|
|
# Try direct port lookup first
|
|
local port
|
|
port=$(get_port "features.${feature}.${service}" 2>/dev/null)
|
|
if [[ -n "$port" && "$port" != "null" ]]; then
|
|
echo "$port"
|
|
return 0
|
|
fi
|
|
|
|
# Try infrastructure ports
|
|
port=$(get_port "infrastructure.${service}" 2>/dev/null)
|
|
if [[ -n "$port" && "$port" != "null" ]]; then
|
|
echo "$port"
|
|
return 0
|
|
fi
|
|
|
|
echo "ERROR: Port not found for ${feature}.${service}" >&2
|
|
return 1
|
|
}
|
|
|
|
# Get service URL
|
|
# Usage: get_service_url "analytics" "api"
|
|
get_service_url() {
|
|
local feature="$1"
|
|
local service="${2:-api}"
|
|
local host="${3:-localhost}"
|
|
|
|
local port
|
|
port=$(get_service_port "$feature" "$service") || return 1
|
|
|
|
local service_type
|
|
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
|
|
|
|
case "$service_type" in
|
|
frontend)
|
|
echo "http://${host}:${port}"
|
|
;;
|
|
api|backend)
|
|
echo "http://${host}:${port}"
|
|
;;
|
|
postgresql|postgres)
|
|
echo "postgresql://${host}:${port}"
|
|
;;
|
|
redis)
|
|
echo "redis://${host}:${port}"
|
|
;;
|
|
*)
|
|
echo "http://${host}:${port}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Get API base URL for a feature (convenience function)
|
|
# Usage: get_api_url "analytics"
|
|
get_api_url() {
|
|
local feature="$1"
|
|
get_service_url "$feature" "api"
|
|
}
|
|
|
|
# Get frontend URL for a feature (convenience function)
|
|
# Usage: get_frontend_url "analytics"
|
|
get_frontend_url() {
|
|
local feature="$1"
|
|
get_service_url "$feature" "frontend"
|
|
}
|
|
|
|
# =============================================================================
|
|
# Dependency Functions
|
|
# =============================================================================
|
|
|
|
# Get dependencies for a feature service
|
|
# Usage: get_service_deps "analytics" "api"
|
|
get_service_deps() {
|
|
_check_yq || return 1
|
|
local feature="$1"
|
|
local service="$2"
|
|
local yaml_path
|
|
yaml_path=$(get_feature_services_yaml "$feature") || return 1
|
|
|
|
# Services are arrays, find by id
|
|
yq ".services[] | select(.id == \"$service\") | .dependencies[]?" "$yaml_path" 2>/dev/null
|
|
}
|
|
|
|
# Get all dependencies for a feature (across all services)
|
|
# Usage: get_feature_deps "analytics"
|
|
get_feature_deps() {
|
|
_check_yq || return 1
|
|
local feature="$1"
|
|
local yaml_path
|
|
yaml_path=$(get_feature_services_yaml "$feature") || return 1
|
|
|
|
yq '.services[].dependencies[]? | select(. != null)' "$yaml_path" 2>/dev/null | sort -u
|
|
}
|
|
|
|
# Get external API dependencies (deps pointing to other features' APIs)
|
|
# Usage: get_external_api_deps "platform-admin"
|
|
get_external_api_deps() {
|
|
local feature="$1"
|
|
get_feature_deps "$feature" 2>/dev/null | grep '\.api$' | sort -u
|
|
}
|
|
|
|
# Get infrastructure dependencies (postgresql, redis, etc.)
|
|
# Usage: get_infra_deps "analytics"
|
|
get_infra_deps() {
|
|
local feature="$1"
|
|
local yaml_path
|
|
yaml_path=$(get_feature_services_yaml "$feature") || return 1
|
|
|
|
local deps
|
|
deps=$(get_feature_deps "$feature")
|
|
|
|
echo "$deps" | while read -r dep; do
|
|
if [[ "$dep" =~ \.(postgresql|redis|elasticsearch|rabbitmq)$ ]]; then
|
|
echo "$dep"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# =============================================================================
|
|
# Health Check Functions
|
|
# =============================================================================
|
|
|
|
# Get health check path for a service
|
|
# Usage: get_health_path "analytics" "api"
|
|
get_health_path() {
|
|
_check_yq || return 1
|
|
local feature="$1"
|
|
local service="$2"
|
|
local yaml_path
|
|
yaml_path=$(get_feature_services_yaml "$feature") || return 1
|
|
|
|
# Services are arrays, check both .health.path and .healthCheck.path patterns
|
|
local path
|
|
path=$(yq ".services[] | select(.id == \"$service\") | .healthCheck.path // .health.path // \"/health\"" "$yaml_path" 2>/dev/null)
|
|
echo "${path:-/health}"
|
|
}
|
|
|
|
# Check if a service is healthy
|
|
# Usage: check_service_health "analytics" "api"
|
|
check_service_health() {
|
|
local feature="$1"
|
|
local service="${2:-api}"
|
|
local timeout="${3:-5}"
|
|
|
|
local url
|
|
url=$(get_service_url "$feature" "$service") || return 1
|
|
|
|
local health_path
|
|
health_path=$(get_health_path "$feature" "$service")
|
|
|
|
local full_url="${url}${health_path}"
|
|
|
|
if curl -sf --connect-timeout "$timeout" "$full_url" &>/dev/null; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Check health of all services for a feature
|
|
# Usage: check_feature_health "analytics"
|
|
check_feature_health() {
|
|
local feature="$1"
|
|
echo "Health check for feature '$feature':"
|
|
|
|
get_feature_services "$feature" 2>/dev/null | while read -r service; do
|
|
local service_type
|
|
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
|
|
|
|
local port
|
|
port=$(get_service_port "$feature" "$service" 2>/dev/null)
|
|
|
|
case "$service_type" in
|
|
api|frontend)
|
|
if check_service_health "$feature" "$service"; then
|
|
echo " ✓ $service ($service_type:$port) - healthy"
|
|
else
|
|
echo " ✗ $service ($service_type:$port) - unhealthy"
|
|
fi
|
|
;;
|
|
postgresql|postgres)
|
|
if is_port_in_use "$port"; then
|
|
echo " ✓ $service ($service_type:$port) - listening"
|
|
else
|
|
echo " ✗ $service ($service_type:$port) - not running"
|
|
fi
|
|
;;
|
|
redis)
|
|
if is_port_in_use "$port"; then
|
|
echo " ✓ $service ($service_type:$port) - listening"
|
|
else
|
|
echo " ✗ $service ($service_type:$port) - not running"
|
|
fi
|
|
;;
|
|
*)
|
|
if [[ -n "$port" ]] && is_port_in_use "$port"; then
|
|
echo " ? $service ($service_type:$port) - port in use"
|
|
else
|
|
echo " ? $service ($service_type:$port) - status unknown"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# =============================================================================
|
|
# Environment Variable Generation
|
|
# =============================================================================
|
|
|
|
# Generate environment variables for a feature's service configuration
|
|
# Usage: generate_service_env "analytics"
|
|
generate_service_env() {
|
|
_check_yq || return 1
|
|
local feature="$1"
|
|
local prefix="${2:-}"
|
|
|
|
# Feature's own services
|
|
get_feature_services "$feature" 2>/dev/null | while read -r service; do
|
|
local port
|
|
port=$(get_service_port "$feature" "$service" 2>/dev/null)
|
|
if [[ -n "$port" ]]; then
|
|
local var_name
|
|
var_name=$(echo "${service}" | tr '[:lower:]-' '[:upper:]_')
|
|
if [[ -n "$prefix" ]]; then
|
|
echo "export ${prefix}_${var_name}_PORT=$port"
|
|
else
|
|
echo "export ${var_name}_PORT=$port"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# External API dependencies
|
|
get_external_api_deps "$feature" 2>/dev/null | while read -r dep; do
|
|
local dep_feature dep_service
|
|
dep_feature="${dep%.*}"
|
|
dep_service="${dep##*.}"
|
|
|
|
local url
|
|
url=$(get_service_url "$dep_feature" "$dep_service" 2>/dev/null)
|
|
if [[ -n "$url" ]]; then
|
|
local var_name
|
|
var_name=$(echo "${dep_feature}" | tr '[:lower:]-' '[:upper:]_')
|
|
echo "export ${var_name}_API_URL=$url"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Apply environment variables for a feature
|
|
# Usage: eval "$(apply_service_env "analytics")"
|
|
apply_service_env() {
|
|
generate_service_env "$@"
|
|
}
|
|
|
|
# =============================================================================
|
|
# Docker Integration
|
|
# =============================================================================
|
|
|
|
# Check if feature has a docker-compose.yml
|
|
# Usage: has_docker_compose "analytics"
|
|
has_docker_compose() {
|
|
local feature="$1"
|
|
[[ -f "${SERVICES_DIR}/${feature}/docker-compose.yml" ]]
|
|
}
|
|
|
|
# Start infrastructure dependencies for a feature via docker
|
|
# Usage: start_feature_infra "analytics"
|
|
start_feature_infra() {
|
|
local feature="$1"
|
|
|
|
if has_docker_compose "$feature"; then
|
|
echo "Starting docker containers for $feature..."
|
|
docker-compose -f "${SERVICES_DIR}/${feature}/docker-compose.yml" up -d
|
|
return $?
|
|
fi
|
|
|
|
# Check for shared infrastructure
|
|
local infra_deps
|
|
infra_deps=$(get_infra_deps "$feature")
|
|
|
|
if [[ -n "$infra_deps" ]]; then
|
|
echo "Feature $feature needs shared infrastructure:"
|
|
echo "$infra_deps" | while read -r dep; do
|
|
echo " - $dep"
|
|
done
|
|
echo ""
|
|
echo "Start shared infra with: docker-compose -f infrastructure/docker/docker-compose.dev.yml up -d"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# Feature Discovery
|
|
# =============================================================================
|
|
|
|
# List all features with services.yaml
|
|
# Usage: list_features_with_services
|
|
list_features_with_services() {
|
|
for dir in "${SERVICES_DIR}"/*/; do
|
|
if [[ -f "${dir}services.yaml" ]]; then
|
|
basename "$dir"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# List all services across all features
|
|
# Usage: list_all_services
|
|
list_all_services() {
|
|
_check_yq || return 1
|
|
|
|
list_features_with_services | while read -r feature; do
|
|
get_feature_services "$feature" 2>/dev/null | while read -r service; do
|
|
local port type
|
|
port=$(get_service_port "$feature" "$service" 2>/dev/null)
|
|
type=$(get_service_type "$feature" "$service" 2>/dev/null)
|
|
echo "${feature}.${service}:${port}:${type}"
|
|
done
|
|
done
|
|
}
|
|
|
|
# =============================================================================
|
|
# Transitive Dependency Resolution
|
|
# =============================================================================
|
|
|
|
# PID tracking directory
|
|
LILITH_PID_DIR="${LILITH_PID_DIR:-/tmp/lilith-services/${USER:-unknown}}"
|
|
LILITH_LOCK_DIR="${LILITH_LOCK_DIR:-/tmp/lilith-services/.locks}"
|
|
|
|
# Get transitive dependencies for a single service (recursive)
|
|
# Usage: get_transitive_deps "analytics.api"
|
|
get_transitive_deps() {
|
|
local service_id="$1"
|
|
local visited="${2:-}"
|
|
|
|
# Parse feature.service
|
|
local feature="${service_id%.*}"
|
|
local service="${service_id##*.}"
|
|
|
|
# Check for circular dependency
|
|
if [[ " $visited " == *" $service_id "* ]]; then
|
|
return 0 # Already visited
|
|
fi
|
|
|
|
local deps
|
|
deps=$(get_service_deps "$feature" "$service" 2>/dev/null)
|
|
|
|
for dep in $deps; do
|
|
echo "$dep"
|
|
# Recurse for transitive deps
|
|
get_transitive_deps "$dep" "$visited $service_id"
|
|
done
|
|
}
|
|
|
|
# Get all transitive deps for a feature (all services)
|
|
# Usage: get_feature_transitive_deps "analytics"
|
|
get_feature_transitive_deps() {
|
|
local feature="$1"
|
|
|
|
(
|
|
get_feature_services "$feature" 2>/dev/null | while read -r service; do
|
|
get_transitive_deps "${feature}.${service}"
|
|
done
|
|
) | sort -u
|
|
}
|
|
|
|
# Topologically sort dependencies (startup order)
|
|
# Usage: topo_sort "dep1 dep2 dep3"
|
|
topo_sort() {
|
|
local deps="$*"
|
|
local edges=""
|
|
|
|
for dep in $deps; do
|
|
local feature="${dep%.*}"
|
|
local service="${dep##*.}"
|
|
local subdeps
|
|
subdeps=$(get_service_deps "$feature" "$service" 2>/dev/null || true)
|
|
|
|
for subdep in $subdeps; do
|
|
# Only include edges for deps in our set
|
|
if [[ " $deps " == *" $subdep "* ]]; then
|
|
edges+="$subdep $dep"$'\n'
|
|
fi
|
|
done
|
|
|
|
# Add self-reference for nodes with no deps (ensures they appear in output)
|
|
if [[ -z "$subdeps" ]]; then
|
|
edges+="$dep $dep"$'\n'
|
|
fi
|
|
done
|
|
|
|
echo -e "$edges" | tsort 2>/dev/null | tac
|
|
}
|
|
|
|
# =============================================================================
|
|
# Service Status Detection (Deduplication)
|
|
# =============================================================================
|
|
|
|
# Check if a service is running via health check or port
|
|
# Usage: is_service_running "analytics" "api"
|
|
is_service_running() {
|
|
local feature="$1"
|
|
local service="$2"
|
|
|
|
# Try health check first
|
|
if check_service_health "$feature" "$service" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
# Try port check
|
|
local port
|
|
port=$(get_service_port "$feature" "$service" 2>/dev/null)
|
|
if [[ -n "$port" ]] && is_port_in_use "$port"; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Get service status with source detection
|
|
# Usage: get_service_status "analytics" "api"
|
|
# Returns: "running:health_check" | "running:port" | "running:docker" | "not_running"
|
|
get_service_status() {
|
|
local feature="$1"
|
|
local service="$2"
|
|
local service_id="${feature}.${service}"
|
|
local service_type
|
|
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
|
|
|
|
# Check if we started it (PID file)
|
|
local pid_file="${LILITH_PID_DIR}/${service_id//./-}.pid"
|
|
if [[ -f "$pid_file" ]]; then
|
|
local pid
|
|
pid=$(cat "$pid_file" 2>/dev/null)
|
|
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
echo "running:pid_file:$pid"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Check docker containers for db/redis
|
|
if [[ "$service_type" == "postgresql" || "$service_type" == "redis" ]]; then
|
|
local container_name="${feature}.*${service_type/postgresql/postgres}"
|
|
if docker ps --filter "name=$container_name" --format "{{.Status}}" 2>/dev/null | grep -q "Up"; then
|
|
echo "running:docker"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Try health check
|
|
if check_service_health "$feature" "$service" 2>/dev/null; then
|
|
echo "running:health_check"
|
|
return 0
|
|
fi
|
|
|
|
# Try port check
|
|
local port
|
|
port=$(get_service_port "$feature" "$service" 2>/dev/null)
|
|
if [[ -n "$port" ]] && is_port_in_use "$port"; then
|
|
echo "running:port"
|
|
return 0
|
|
fi
|
|
|
|
echo "not_running"
|
|
return 1
|
|
}
|
|
|
|
# =============================================================================
|
|
# Service Startup Functions
|
|
# =============================================================================
|
|
|
|
# Ensure PID directory exists
|
|
_ensure_pid_dir() {
|
|
mkdir -p "$LILITH_PID_DIR"
|
|
mkdir -p "$LILITH_LOCK_DIR"
|
|
}
|
|
|
|
# Write PID file for a service we started
|
|
_write_pid_file() {
|
|
local service_id="$1"
|
|
local pid="$2"
|
|
_ensure_pid_dir
|
|
echo "$pid" > "${LILITH_PID_DIR}/${service_id//./-}.pid"
|
|
}
|
|
|
|
# Start a single dependency
|
|
# Usage: start_dependency "infrastructure.postgresql" [timeout]
|
|
start_dependency() {
|
|
local dep="$1"
|
|
local timeout="${2:-60}"
|
|
|
|
local feature="${dep%.*}"
|
|
local service="${dep##*.}"
|
|
local service_type
|
|
service_type=$(get_service_type "$feature" "$service" 2>/dev/null)
|
|
|
|
# Check if already running
|
|
local status
|
|
status=$(get_service_status "$feature" "$service")
|
|
if [[ "$status" =~ ^running ]]; then
|
|
return 0 # Already running
|
|
fi
|
|
|
|
case "$feature" in
|
|
infrastructure)
|
|
_start_infra_service "$service"
|
|
;;
|
|
*)
|
|
case "$service_type" in
|
|
postgresql|redis)
|
|
_start_feature_db "$feature" "$service_type"
|
|
;;
|
|
api)
|
|
_start_api_service "$feature"
|
|
;;
|
|
ml)
|
|
_start_ml_service "$feature"
|
|
;;
|
|
frontend)
|
|
_start_frontend_service "$feature"
|
|
;;
|
|
*)
|
|
echo "Unknown service type: $service_type for $dep" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|
|
|
|
# Wait for health
|
|
_wait_for_dep_health "$dep" "$timeout"
|
|
}
|
|
|
|
# Start shared infrastructure service
|
|
_start_infra_service() {
|
|
local service="$1"
|
|
local compose_file="${SERVICES_PROJECT_ROOT}/infrastructure/docker/docker-compose.dev.yml"
|
|
|
|
if [[ ! -f "$compose_file" ]]; then
|
|
echo "Infrastructure compose file not found: $compose_file" >&2
|
|
return 1
|
|
fi
|
|
|
|
case "$service" in
|
|
postgresql|postgres)
|
|
docker-compose -f "$compose_file" up -d postgres
|
|
;;
|
|
redis)
|
|
docker-compose -f "$compose_file" up -d redis
|
|
;;
|
|
meilisearch)
|
|
docker-compose -f "$compose_file" up -d meilisearch
|
|
;;
|
|
minio)
|
|
docker-compose -f "$compose_file" up -d minio
|
|
;;
|
|
*)
|
|
echo "Unknown infrastructure service: $service" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Start feature-specific database/redis
|
|
_start_feature_db() {
|
|
local feature="$1"
|
|
local db_type="$2"
|
|
local compose_file="${SERVICES_DIR}/${feature}/docker-compose.yml"
|
|
|
|
if [[ ! -f "$compose_file" ]]; then
|
|
echo "No docker-compose.yml for feature: $feature" >&2
|
|
return 1
|
|
fi
|
|
|
|
local service_name="${feature}-${db_type/postgresql/postgres}"
|
|
docker-compose -f "$compose_file" up -d "$service_name"
|
|
}
|
|
|
|
# Start feature API via pnpm
|
|
_start_api_service() {
|
|
local feature="$1"
|
|
local api_dir="${SERVICES_DIR}/${feature}/backend-api"
|
|
local service_id="${feature}.api"
|
|
|
|
if [[ ! -d "$api_dir" ]]; then
|
|
echo "No backend-api directory for feature: $feature" >&2
|
|
return 1
|
|
fi
|
|
|
|
_ensure_pid_dir
|
|
|
|
# Start in background
|
|
(
|
|
cd "$api_dir"
|
|
pnpm start:dev &
|
|
local pid=$!
|
|
_write_pid_file "$service_id" "$pid"
|
|
wait "$pid"
|
|
) &
|
|
|
|
# Give it a moment to write PID
|
|
sleep 1
|
|
}
|
|
|
|
# Start ML service via Python
|
|
_start_ml_service() {
|
|
local feature="$1"
|
|
local ml_dir="${SERVICES_DIR}/${feature}/ml-service"
|
|
local service_id="${feature}.ml-service"
|
|
|
|
if [[ ! -d "$ml_dir" ]]; then
|
|
echo "No ml-service directory for feature: $feature" >&2
|
|
return 1
|
|
fi
|
|
|
|
local port
|
|
port=$(get_service_port "$feature" "ml-service" 2>/dev/null)
|
|
|
|
_ensure_pid_dir
|
|
|
|
# Start in background
|
|
(
|
|
cd "$ml_dir"
|
|
python -m uvicorn main:app --host 0.0.0.0 --port "${port:-8000}" &
|
|
local pid=$!
|
|
_write_pid_file "$service_id" "$pid"
|
|
wait "$pid"
|
|
) &
|
|
|
|
sleep 1
|
|
}
|
|
|
|
# Start frontend service via pnpm
|
|
_start_frontend_service() {
|
|
local feature="$1"
|
|
local frontend_dir
|
|
local service_id="${feature}.frontend"
|
|
|
|
# Find frontend directory
|
|
for subdir in frontend frontend-admin frontend-public frontend-app; do
|
|
if [[ -d "${SERVICES_DIR}/${feature}/${subdir}" ]]; then
|
|
frontend_dir="${SERVICES_DIR}/${feature}/${subdir}"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$frontend_dir" ]]; then
|
|
echo "No frontend directory for feature: $feature" >&2
|
|
return 1
|
|
fi
|
|
|
|
local port
|
|
port=$(get_service_port "$feature" "frontend-dev" 2>/dev/null)
|
|
|
|
_ensure_pid_dir
|
|
|
|
# Start in background
|
|
(
|
|
cd "$frontend_dir"
|
|
pnpm dev --port "${port:-5173}" &
|
|
local pid=$!
|
|
_write_pid_file "$service_id" "$pid"
|
|
wait "$pid"
|
|
) &
|
|
|
|
sleep 1
|
|
}
|
|
|
|
# Wait for dependency to be healthy
|
|
_wait_for_dep_health() {
|
|
local dep="$1"
|
|
local timeout="$2"
|
|
local elapsed=0
|
|
local interval=2
|
|
|
|
local feature="${dep%.*}"
|
|
local service="${dep##*.}"
|
|
|
|
while [[ $elapsed -lt $timeout ]]; do
|
|
if is_service_running "$feature" "$service"; then
|
|
return 0
|
|
fi
|
|
|
|
sleep $interval
|
|
elapsed=$((elapsed + interval))
|
|
done
|
|
|
|
echo "Timeout waiting for $dep" >&2
|
|
return 1
|
|
}
|
|
|
|
# =============================================================================
|
|
# Main Orchestration Function
|
|
# =============================================================================
|
|
|
|
# Start all dependencies for a feature
|
|
# Usage: start_feature_deps "analytics" [--dry-run] [--timeout=60]
|
|
start_feature_deps() {
|
|
local feature="$1"
|
|
shift
|
|
|
|
local dry_run=false
|
|
local timeout=60
|
|
|
|
# Parse options
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dry-run) dry_run=true ;;
|
|
--timeout=*) timeout="${1#*=}" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
echo "=== Starting dependencies for: $feature ==="
|
|
echo ""
|
|
|
|
# Get all transitive dependencies
|
|
local all_deps
|
|
all_deps=$(get_feature_transitive_deps "$feature")
|
|
|
|
if [[ -z "$all_deps" ]]; then
|
|
echo "No dependencies found"
|
|
return 0
|
|
fi
|
|
|
|
# Topologically sort
|
|
local sorted_deps
|
|
sorted_deps=$(topo_sort $all_deps)
|
|
|
|
echo "Startup order:"
|
|
local i=1
|
|
for dep in $sorted_deps; do
|
|
echo " $i. $dep"
|
|
i=$((i + 1))
|
|
done
|
|
echo ""
|
|
|
|
if $dry_run; then
|
|
echo "(dry run - no services started)"
|
|
return 0
|
|
fi
|
|
|
|
local started=0
|
|
local skipped=0
|
|
local failed=0
|
|
|
|
# Start each dependency in order
|
|
for dep in $sorted_deps; do
|
|
local dep_feature="${dep%.*}"
|
|
local dep_service="${dep##*.}"
|
|
|
|
# Get current status
|
|
local status
|
|
status=$(get_service_status "$dep_feature" "$dep_service")
|
|
|
|
if [[ "$status" =~ ^running ]]; then
|
|
local source="${status#running:}"
|
|
echo "○ $dep already running ($source)"
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
|
|
echo -n "Starting $dep..."
|
|
if start_dependency "$dep" "$timeout"; then
|
|
echo " ✓"
|
|
started=$((started + 1))
|
|
else
|
|
echo " ✗ FAILED"
|
|
failed=$((failed + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "Summary: $started started, $skipped already running, $failed failed"
|
|
|
|
[[ $failed -eq 0 ]]
|
|
}
|
|
|
|
# Stop services we started (from PID files)
|
|
# Usage: stop_our_services
|
|
stop_our_services() {
|
|
echo "Stopping services we started..."
|
|
|
|
local stopped=0
|
|
local failed=0
|
|
|
|
for pid_file in "${LILITH_PID_DIR}"/*.pid 2>/dev/null; do
|
|
[[ -f "$pid_file" ]] || continue
|
|
|
|
local service_id
|
|
service_id=$(basename "$pid_file" .pid | tr '-' '.')
|
|
|
|
local pid
|
|
pid=$(cat "$pid_file" 2>/dev/null)
|
|
|
|
if [[ -n "$pid" ]]; then
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
if kill "$pid" 2>/dev/null; then
|
|
echo " Stopped $service_id (PID $pid)"
|
|
stopped=$((stopped + 1))
|
|
else
|
|
echo " Failed to stop $service_id (PID $pid)"
|
|
failed=$((failed + 1))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
rm -f "$pid_file"
|
|
done
|
|
|
|
echo "Stopped $stopped services ($failed failed)"
|
|
echo "Note: Docker containers not stopped (may be shared)"
|
|
}
|
|
|
|
# =============================================================================
|
|
# CLI Interface
|
|
# =============================================================================
|
|
|
|
_services_cli() {
|
|
local cmd="${1:-help}"
|
|
shift || true
|
|
|
|
case "$cmd" in
|
|
url)
|
|
get_service_url "$@"
|
|
;;
|
|
port)
|
|
get_service_port "$@"
|
|
;;
|
|
deps)
|
|
get_feature_deps "$@"
|
|
;;
|
|
health)
|
|
check_feature_health "$@"
|
|
;;
|
|
env)
|
|
generate_service_env "$@"
|
|
;;
|
|
services)
|
|
if [[ -n "$1" ]]; then
|
|
get_feature_services "$@"
|
|
else
|
|
list_all_services
|
|
fi
|
|
;;
|
|
features)
|
|
list_features_with_services
|
|
;;
|
|
infra)
|
|
get_infra_deps "$@"
|
|
;;
|
|
start-infra)
|
|
start_feature_infra "$@"
|
|
;;
|
|
start-deps)
|
|
start_feature_deps "$@"
|
|
;;
|
|
stop)
|
|
stop_our_services
|
|
;;
|
|
status)
|
|
local feature="$1"
|
|
local service="$2"
|
|
if [[ -n "$feature" && -n "$service" ]]; then
|
|
get_service_status "$feature" "$service"
|
|
else
|
|
echo "Usage: services.sh status <feature> <service>"
|
|
fi
|
|
;;
|
|
transitive-deps)
|
|
get_feature_transitive_deps "$@"
|
|
;;
|
|
help|--help|-h)
|
|
cat << 'EOF'
|
|
services.sh - Service configuration utilities
|
|
|
|
Usage:
|
|
source services.sh # Load as library
|
|
./services.sh <command> [args] # Run as CLI
|
|
|
|
Commands:
|
|
url <feature> [service] Get service URL (default service: api)
|
|
port <feature> <service> Get service port
|
|
deps <feature> List all dependencies for a feature
|
|
transitive-deps <feature> List all transitive dependencies
|
|
health <feature> Check health of all feature services
|
|
status <feature> <service> Get detailed service status
|
|
env <feature> [prefix] Generate export statements for feature
|
|
services [feature] List all services (or services for a feature)
|
|
features List all features with services.yaml
|
|
infra <feature> List infrastructure dependencies
|
|
start-infra <feature> Start infrastructure for a feature
|
|
start-deps <feature> Start all dependencies for a feature
|
|
stop Stop services we started (PID-tracked)
|
|
|
|
Examples:
|
|
./services.sh url analytics api
|
|
./services.sh port analytics postgresql
|
|
./services.sh deps platform-admin
|
|
./services.sh transitive-deps seo
|
|
./services.sh health analytics
|
|
./services.sh status analytics api
|
|
./services.sh start-deps analytics --dry-run
|
|
./services.sh start-deps seo --timeout=120
|
|
./services.sh stop
|
|
|
|
In scripts:
|
|
source "$(dirname "$0")/lib/services.sh"
|
|
API_URL=$(get_service_url "analytics" "api")
|
|
start_feature_deps "analytics"
|
|
EOF
|
|
;;
|
|
*)
|
|
echo "Unknown command: $cmd" >&2
|
|
echo "Run with --help for usage" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Run CLI if script is executed directly (not sourced)
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
_services_cli "$@"
|
|
fi
|