chore: add install scripts, doctor command, and AGENTS.md
- Add one-line installer (scripts/install.sh) with platform detection - Add doctor command (scripts/commands/doctor.sh) for environment diagnostics - Add shared libraries: dependencies, docker, platform, validation - Update README with quick-start installer instructions - Add AGENTS.md with codebase patterns for AI agent context Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
491
scripts/lib/docker.sh
Normal file
491
scripts/lib/docker.sh
Normal file
@@ -0,0 +1,491 @@
|
||||
#!/bin/bash
|
||||
# Docker-specific functions for Mosaic Stack installer
|
||||
# Handles Docker Compose operations, health checks, and service management
|
||||
|
||||
# shellcheck source=lib/platform.sh
|
||||
source "${BASH_SOURCE[0]%/*}/platform.sh"
|
||||
|
||||
# ============================================================================
|
||||
# Docker Compose Helpers
|
||||
# ============================================================================
|
||||
|
||||
# Get the docker compose command (handles both plugin and standalone)
|
||||
docker_compose_cmd() {
|
||||
if docker compose version &>/dev/null; then
|
||||
echo "docker compose"
|
||||
elif command -v docker-compose &>/dev/null; then
|
||||
echo "docker-compose"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run docker compose with all arguments
|
||||
docker_compose() {
|
||||
local cmd
|
||||
cmd=$(docker_compose_cmd) || {
|
||||
echo -e "${ERROR}Error: Docker Compose not available${NC}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
$cmd "$@"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Service Management
|
||||
# ============================================================================
|
||||
|
||||
# Pull all images defined in docker-compose.yml
|
||||
docker_pull_images() {
|
||||
local compose_file="${1:-docker-compose.yml}"
|
||||
local env_file="${2:-.env}"
|
||||
|
||||
echo -e "${WARN}→${NC} Pulling Docker images..."
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
docker_compose -f "$compose_file" --env-file "$env_file" pull
|
||||
else
|
||||
docker_compose -f "$compose_file" pull
|
||||
fi
|
||||
}
|
||||
|
||||
# Start services with Docker Compose
|
||||
docker_compose_up() {
|
||||
local compose_file="${1:-docker-compose.yml}"
|
||||
local env_file="${2:-.env}"
|
||||
local profiles="${3:-}"
|
||||
local detached="${4:-true}"
|
||||
|
||||
echo -e "${WARN}→${NC} Starting services..."
|
||||
|
||||
local args=("-f" "$compose_file")
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
args+=("--env-file" "$env_file")
|
||||
fi
|
||||
|
||||
if [[ -n "$profiles" ]]; then
|
||||
args+=("--profile" "$profiles")
|
||||
fi
|
||||
|
||||
if [[ "$detached" == "true" ]]; then
|
||||
args+=("up" "-d")
|
||||
else
|
||||
args+=("up")
|
||||
fi
|
||||
|
||||
docker_compose "${args[@]}"
|
||||
}
|
||||
|
||||
# Stop services
|
||||
docker_compose_down() {
|
||||
local compose_file="${1:-docker-compose.yml}"
|
||||
local env_file="${2:-.env}"
|
||||
|
||||
echo -e "${WARN}→${NC} Stopping services..."
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
docker_compose -f "$compose_file" --env-file "$env_file" down
|
||||
else
|
||||
docker_compose -f "$compose_file" down
|
||||
fi
|
||||
}
|
||||
|
||||
# Restart services
|
||||
docker_compose_restart() {
|
||||
local compose_file="${1:-docker-compose.yml}"
|
||||
local env_file="${2:-.env}"
|
||||
|
||||
echo -e "${WARN}→${NC} Restarting services..."
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
docker_compose -f "$compose_file" --env-file "$env_file" restart
|
||||
else
|
||||
docker_compose -f "$compose_file" restart
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Health Checks
|
||||
# ============================================================================
|
||||
|
||||
# Wait for a container to be healthy
|
||||
wait_for_healthy_container() {
|
||||
local container_name="$1"
|
||||
local timeout="${2:-120}"
|
||||
local interval="${3:-5}"
|
||||
|
||||
echo -e "${INFO}i${NC} Waiting for ${INFO}$container_name${NC} to be healthy..."
|
||||
|
||||
local elapsed=0
|
||||
while [[ $elapsed -lt $timeout ]]; do
|
||||
local status
|
||||
status=$(docker inspect --format='{{.State.Health.Status}}' "$container_name" 2>/dev/null || echo "not_found")
|
||||
|
||||
case "$status" in
|
||||
healthy)
|
||||
echo -e "${SUCCESS}✓${NC} $container_name is healthy"
|
||||
return 0
|
||||
;;
|
||||
unhealthy)
|
||||
echo -e "${ERROR}✗${NC} $container_name is unhealthy"
|
||||
return 1
|
||||
;;
|
||||
not_found)
|
||||
echo -e "${WARN}→${NC} Container $container_name not found"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
sleep "$interval"
|
||||
((elapsed += interval))
|
||||
done
|
||||
|
||||
echo -e "${ERROR}✗${NC} Timeout waiting for $container_name to be healthy"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Wait for multiple containers to be healthy
|
||||
wait_for_healthy_containers() {
|
||||
local containers=("$@")
|
||||
local timeout="${containers[-1]}"
|
||||
unset 'containers[-1]'
|
||||
|
||||
for container in "${containers[@]}"; do
|
||||
if ! wait_for_healthy_container "$container" "$timeout"; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Wait for a service to respond on a port
|
||||
wait_for_service() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local name="$3"
|
||||
local timeout="${4:-60}"
|
||||
|
||||
echo -e "${INFO}i${NC} Waiting for ${INFO}$name${NC} at $host:$port..."
|
||||
|
||||
local elapsed=0
|
||||
while [[ $elapsed -lt $timeout ]]; do
|
||||
if docker run --rm --network host alpine:latest nc -z "$host" "$port" 2>/dev/null; then
|
||||
echo -e "${SUCCESS}✓${NC} $name is responding"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
((elapsed += 2))
|
||||
done
|
||||
|
||||
echo -e "${ERROR}✗${NC} Timeout waiting for $name"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Container Status
|
||||
# ============================================================================
|
||||
|
||||
# Get container status
|
||||
get_container_status() {
|
||||
local container_name="$1"
|
||||
|
||||
docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null || echo "not_found"
|
||||
}
|
||||
|
||||
# Check if container is running
|
||||
is_container_running() {
|
||||
local container_name="$1"
|
||||
local status
|
||||
status=$(get_container_status "$container_name")
|
||||
[[ "$status" == "running" ]]
|
||||
}
|
||||
|
||||
# List all Mosaic containers
|
||||
list_mosaic_containers() {
|
||||
docker ps -a --filter "name=mosaic-" --format "{{.Names}}\t{{.Status}}"
|
||||
}
|
||||
|
||||
# Get container logs
|
||||
get_container_logs() {
|
||||
local container_name="$1"
|
||||
local lines="${2:-100}"
|
||||
|
||||
docker logs --tail "$lines" "$container_name" 2>&1
|
||||
}
|
||||
|
||||
# Tail container logs
|
||||
tail_container_logs() {
|
||||
local container_name="$1"
|
||||
|
||||
docker logs -f "$container_name"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Database Operations
|
||||
# ============================================================================
|
||||
|
||||
# Wait for PostgreSQL to be ready
|
||||
wait_for_postgres() {
|
||||
local container_name="${1:-mosaic-postgres}"
|
||||
local user="${2:-mosaic}"
|
||||
local database="${3:-mosaic}"
|
||||
local timeout="${4:-60}"
|
||||
|
||||
echo -e "${INFO}i${NC} Waiting for PostgreSQL to be ready..."
|
||||
|
||||
local elapsed=0
|
||||
while [[ $elapsed -lt $timeout ]]; do
|
||||
if docker exec "$container_name" pg_isready -U "$user" -d "$database" &>/dev/null; then
|
||||
echo -e "${SUCCESS}✓${NC} PostgreSQL is ready"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
((elapsed += 2))
|
||||
done
|
||||
|
||||
echo -e "${ERROR}✗${NC} Timeout waiting for PostgreSQL"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run database migrations
|
||||
run_database_migrations() {
|
||||
local api_container="${1:-mosaic-api}"
|
||||
|
||||
echo -e "${WARN}→${NC} Running database migrations..."
|
||||
|
||||
if ! docker exec "$api_container" npx prisma migrate deploy &>/dev/null; then
|
||||
echo -e "${WARN}→${NC} Could not run migrations via API container"
|
||||
echo -e "${INFO}i${NC} Migrations will run automatically when API starts"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${SUCCESS}✓${NC} Database migrations complete"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Service URLs
|
||||
# ============================================================================
|
||||
|
||||
# Get the URL for a service
|
||||
get_service_url() {
|
||||
local service="$1"
|
||||
local port="${2:-}"
|
||||
|
||||
local host="localhost"
|
||||
|
||||
# Check if we're in WSL and need to use Windows host
|
||||
if is_wsl; then
|
||||
host=$(cat /etc/resolv.conf 2>/dev/null | grep nameserver | awk '{print $2}' | head -1)
|
||||
fi
|
||||
|
||||
if [[ -n "$port" ]]; then
|
||||
echo "http://${host}:${port}"
|
||||
else
|
||||
echo "http://${host}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get all service URLs
|
||||
get_all_service_urls() {
|
||||
local env_file="${1:-.env}"
|
||||
|
||||
declare -A urls=()
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$env_file"
|
||||
fi
|
||||
|
||||
urls[web]="http://localhost:${WEB_PORT:-3000}"
|
||||
urls[api]="http://localhost:${API_PORT:-3001}"
|
||||
urls[postgres]="localhost:${POSTGRES_PORT:-5432}"
|
||||
urls[valkey]="localhost:${VALKEY_PORT:-6379}"
|
||||
|
||||
if [[ "${OIDC_ENABLED:-false}" == "true" ]]; then
|
||||
urls[authentik]="http://localhost:${AUTHENTIK_PORT_HTTP:-9000}"
|
||||
fi
|
||||
|
||||
if [[ "${OLLAMA_MODE:-disabled}" != "disabled" ]]; then
|
||||
urls[ollama]="http://localhost:${OLLAMA_PORT:-11434}"
|
||||
fi
|
||||
|
||||
for service in "${!urls[@]}"; do
|
||||
echo "$service: ${urls[$service]}"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Docker Cleanup
|
||||
# ============================================================================
|
||||
|
||||
# Remove unused Docker resources
|
||||
docker_cleanup() {
|
||||
echo -e "${WARN}→${NC} Cleaning up unused Docker resources..."
|
||||
|
||||
# Remove dangling images
|
||||
docker image prune -f
|
||||
|
||||
# Remove unused networks
|
||||
docker network prune -f
|
||||
|
||||
echo -e "${SUCCESS}✓${NC} Docker cleanup complete"
|
||||
}
|
||||
|
||||
# Remove all Mosaic containers and volumes
|
||||
docker_remove_all() {
|
||||
local compose_file="${1:-docker-compose.yml}"
|
||||
local env_file="${2:-.env}"
|
||||
|
||||
echo -e "${WARN}→${NC} Removing all Mosaic containers and volumes..."
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
docker_compose -f "$compose_file" --env-file "$env_file" down -v --remove-orphans
|
||||
else
|
||||
docker_compose -f "$compose_file" down -v --remove-orphans
|
||||
fi
|
||||
|
||||
echo -e "${SUCCESS}✓${NC} All containers and volumes removed"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Docker Info
|
||||
# ============================================================================
|
||||
|
||||
# Print Docker system info
|
||||
print_docker_info() {
|
||||
echo -e "${BOLD}Docker Information:${NC}"
|
||||
echo ""
|
||||
|
||||
echo -e " Docker Version:"
|
||||
docker --version 2>/dev/null | sed 's/^/ /'
|
||||
|
||||
echo ""
|
||||
echo -e " Docker Compose:"
|
||||
docker_compose version 2>/dev/null | sed 's/^/ /'
|
||||
|
||||
echo ""
|
||||
echo -e " Docker Storage:"
|
||||
docker system df 2>/dev/null | sed 's/^/ /'
|
||||
|
||||
echo ""
|
||||
echo -e " Running Containers:"
|
||||
docker ps --format " {{.Names}}\t{{.Status}}" 2>/dev/null | head -10
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Volume Management
|
||||
# ============================================================================
|
||||
|
||||
# List all Mosaic volumes
|
||||
list_mosaic_volumes() {
|
||||
docker volume ls --filter "name=mosaic" --format "{{.Name}}"
|
||||
}
|
||||
|
||||
# Backup a Docker volume
|
||||
backup_volume() {
|
||||
local volume_name="$1"
|
||||
local backup_file="${2:-${volume_name}-backup-$(date +%Y%m%d-%H%M%S).tar.gz}"
|
||||
|
||||
echo -e "${WARN}→${NC} Backing up volume ${INFO}$volume_name${NC}..."
|
||||
|
||||
docker run --rm \
|
||||
-v "$volume_name":/source:ro \
|
||||
-v "$(pwd)":/backup \
|
||||
alpine:latest \
|
||||
tar czf "/backup/$backup_file" -C /source .
|
||||
|
||||
echo -e "${SUCCESS}✓${NC} Backup created: $backup_file"
|
||||
}
|
||||
|
||||
# Restore a Docker volume
|
||||
restore_volume() {
|
||||
local volume_name="$1"
|
||||
local backup_file="$2"
|
||||
|
||||
echo -e "${WARN}→${NC} Restoring volume ${INFO}$volume_name${NC} from $backup_file..."
|
||||
|
||||
# Create volume if it doesn't exist
|
||||
docker volume create "$volume_name" &>/dev/null || true
|
||||
|
||||
docker run --rm \
|
||||
-v "$volume_name":/target \
|
||||
-v "$(pwd)":/backup \
|
||||
alpine:latest \
|
||||
tar xzf "/backup/$backup_file" -C /target
|
||||
|
||||
echo -e "${SUCCESS}✓${NC} Volume restored"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Network Management
|
||||
# ============================================================================
|
||||
|
||||
# Create a Docker network if it doesn't exist
|
||||
ensure_network() {
|
||||
local network_name="$1"
|
||||
|
||||
if ! docker network inspect "$network_name" &>/dev/null; then
|
||||
echo -e "${WARN}→${NC} Creating network ${INFO}$network_name${NC}..."
|
||||
docker network create "$network_name"
|
||||
echo -e "${SUCCESS}✓${NC} Network created"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a network exists
|
||||
network_exists() {
|
||||
local network_name="$1"
|
||||
docker network inspect "$network_name" &>/dev/null
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Build Operations
|
||||
# ============================================================================
|
||||
|
||||
# Build Docker images
|
||||
docker_build() {
|
||||
local compose_file="${1:-docker-compose.yml}"
|
||||
local env_file="${2:-.env}"
|
||||
local parallel="${3:-true}"
|
||||
|
||||
echo -e "${WARN}→${NC} Building Docker images..."
|
||||
|
||||
local args=("-f" "$compose_file")
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
args+=("--env-file" "$env_file")
|
||||
fi
|
||||
|
||||
args+=("build")
|
||||
|
||||
if [[ "$parallel" == "true" ]]; then
|
||||
args+=("--parallel")
|
||||
fi
|
||||
|
||||
docker_compose "${args[@]}"
|
||||
}
|
||||
|
||||
# Check if buildx is available
|
||||
check_buildx() {
|
||||
docker buildx version &>/dev/null
|
||||
}
|
||||
|
||||
# Set up buildx builder
|
||||
setup_buildx() {
|
||||
if ! check_buildx; then
|
||||
echo -e "${WARN}→${NC} buildx not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create or use existing builder
|
||||
if ! docker buildx inspect mosaic-builder &>/dev/null; then
|
||||
echo -e "${WARN}→${NC} Creating buildx builder..."
|
||||
docker buildx create --name mosaic-builder --use
|
||||
else
|
||||
docker buildx use mosaic-builder
|
||||
fi
|
||||
}
|
||||
Reference in New Issue
Block a user