#!/usr/bin/env bash # ============================================================================= # CI DOCKER UTILITIES # ============================================================================= # Docker-related utility functions for local CI testing. # # Usage: # source "$SCRIPT_DIR/lib/ci-docker.sh" # # ============================================================================= # Prevent multiple sourcing [[ -n "${_CI_DOCKER_LOADED:-}" ]] && return _CI_DOCKER_LOADED=1 # ============================================================================= # CONFIGURATION # ============================================================================= CI_COMPOSE_FILE="${CI_COMPOSE_FILE:-devops/compose/docker-compose.ci.yaml}" CI_IMAGE="${CI_IMAGE:-stellaops-ci:local}" CI_DOCKERFILE="${CI_DOCKERFILE:-devops/docker/Dockerfile.ci}" CI_PROJECT_NAME="${CI_PROJECT_NAME:-stellaops-ci}" # Service names from docker-compose.ci.yaml CI_SERVICES=(postgres-ci valkey-ci nats-ci mock-registry minio-ci) # ============================================================================= # DOCKER CHECK # ============================================================================= # Check if Docker is available and running check_docker() { if ! command -v docker &>/dev/null; then log_error "Docker is not installed or not in PATH" log_info "Install Docker: https://docs.docker.com/get-docker/" return 1 fi if ! docker info &>/dev/null; then log_error "Docker daemon is not running" log_info "Start Docker Desktop or run: sudo systemctl start docker" return 1 fi log_debug "Docker is available and running" return 0 } # Check if Docker Compose is available check_docker_compose() { if docker compose version &>/dev/null; then DOCKER_COMPOSE="docker compose" log_debug "Using Docker Compose plugin" return 0 elif command -v docker-compose &>/dev/null; then DOCKER_COMPOSE="docker-compose" log_debug "Using standalone docker-compose" return 0 else log_error "Docker Compose is not installed" log_info "Install with: docker compose plugin or standalone docker-compose" return 1 fi } # ============================================================================= # CI SERVICES MANAGEMENT # ============================================================================= # Start CI services start_ci_services() { local services=("$@") local compose_file="$REPO_ROOT/$CI_COMPOSE_FILE" if [[ ! -f "$compose_file" ]]; then log_error "Compose file not found: $compose_file" return 1 fi check_docker || return 1 check_docker_compose || return 1 log_section "Starting CI Services" if [[ ${#services[@]} -eq 0 ]]; then # Start all services log_info "Starting all CI services..." $DOCKER_COMPOSE -f "$compose_file" -p "$CI_PROJECT_NAME" up -d else # Start specific services log_info "Starting services: ${services[*]}" $DOCKER_COMPOSE -f "$compose_file" -p "$CI_PROJECT_NAME" up -d "${services[@]}" fi local result=$? if [[ $result -ne 0 ]]; then log_error "Failed to start CI services" return $result fi # Wait for services to be healthy wait_for_services "${services[@]}" } # Stop CI services stop_ci_services() { local compose_file="$REPO_ROOT/$CI_COMPOSE_FILE" if [[ ! -f "$compose_file" ]]; then log_debug "Compose file not found, nothing to stop" return 0 fi check_docker_compose || return 1 log_section "Stopping CI Services" $DOCKER_COMPOSE -f "$compose_file" -p "$CI_PROJECT_NAME" down } # Stop CI services and remove volumes cleanup_ci_services() { local compose_file="$REPO_ROOT/$CI_COMPOSE_FILE" if [[ ! -f "$compose_file" ]]; then return 0 fi check_docker_compose || return 1 log_section "Cleaning Up CI Services" $DOCKER_COMPOSE -f "$compose_file" -p "$CI_PROJECT_NAME" down -v --remove-orphans } # Check status of CI services check_ci_services_status() { local compose_file="$REPO_ROOT/$CI_COMPOSE_FILE" check_docker_compose || return 1 log_subsection "CI Services Status" $DOCKER_COMPOSE -f "$compose_file" -p "$CI_PROJECT_NAME" ps } # ============================================================================= # HEALTH CHECKS # ============================================================================= # Wait for a specific service to be healthy wait_for_service() { local service="$1" local timeout="${2:-60}" local interval="${3:-2}" log_info "Waiting for $service to be healthy..." local elapsed=0 while [[ $elapsed -lt $timeout ]]; do local status status=$(docker inspect --format='{{.State.Health.Status}}' "${CI_PROJECT_NAME}-${service}-1" 2>/dev/null || echo "not found") if [[ "$status" == "healthy" ]]; then log_success "$service is healthy" return 0 elif [[ "$status" == "not found" ]]; then # Container might not have health check, check if running local running running=$(docker inspect --format='{{.State.Running}}' "${CI_PROJECT_NAME}-${service}-1" 2>/dev/null || echo "false") if [[ "$running" == "true" ]]; then log_success "$service is running (no health check)" return 0 fi fi sleep "$interval" elapsed=$((elapsed + interval)) done log_error "$service did not become healthy within ${timeout}s" return 1 } # Wait for multiple services to be healthy wait_for_services() { local services=("$@") local failed=0 if [[ ${#services[@]} -eq 0 ]]; then services=("${CI_SERVICES[@]}") fi log_info "Waiting for services to be ready..." for service in "${services[@]}"; do if ! wait_for_service "$service" 60 2; then failed=1 fi done return $failed } # Check if PostgreSQL is accepting connections check_postgres_ready() { local host="${1:-localhost}" local port="${2:-5433}" local user="${3:-stellaops_ci}" local db="${4:-stellaops_test}" if command -v pg_isready &>/dev/null; then pg_isready -h "$host" -p "$port" -U "$user" -d "$db" &>/dev/null else # Fallback to nc if pg_isready not available nc -z "$host" "$port" &>/dev/null fi } # Check if Valkey/Redis is accepting connections check_valkey_ready() { local host="${1:-localhost}" local port="${2:-6380}" if command -v valkey-cli &>/dev/null; then valkey-cli -h "$host" -p "$port" ping &>/dev/null elif command -v redis-cli &>/dev/null; then redis-cli -h "$host" -p "$port" ping &>/dev/null else nc -z "$host" "$port" &>/dev/null fi } # ============================================================================= # CI DOCKER IMAGE MANAGEMENT # ============================================================================= # Check if CI image exists ci_image_exists() { docker image inspect "$CI_IMAGE" &>/dev/null } # Build CI Docker image build_ci_image() { local force_rebuild="${1:-false}" local dockerfile="$REPO_ROOT/$CI_DOCKERFILE" if [[ ! -f "$dockerfile" ]]; then log_error "Dockerfile not found: $dockerfile" return 1 fi check_docker || return 1 if ci_image_exists && [[ "$force_rebuild" != "true" ]]; then log_info "CI image already exists: $CI_IMAGE" log_info "Use --rebuild to force rebuild" return 0 fi log_section "Building CI Docker Image" log_info "Dockerfile: $dockerfile" log_info "Image: $CI_IMAGE" docker build -t "$CI_IMAGE" -f "$dockerfile" "$REPO_ROOT" if [[ $? -ne 0 ]]; then log_error "Failed to build CI image" return 1 fi log_success "CI image built successfully: $CI_IMAGE" } # ============================================================================= # CONTAINER EXECUTION # ============================================================================= # Run a command inside the CI container run_in_ci_container() { local command="$*" check_docker || return 1 if ! ci_image_exists; then log_info "CI image not found, building..." build_ci_image || return 1 fi local docker_args=( --rm -v "$REPO_ROOT:/src" -v "$REPO_ROOT/TestResults:/src/TestResults" -e DOTNET_NOLOGO=1 -e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 -e TZ=UTC -w /src ) # Mount Docker socket for Testcontainers if [[ -S /var/run/docker.sock ]]; then docker_args+=(-v /var/run/docker.sock:/var/run/docker.sock) fi # Load environment file if exists local env_file="$REPO_ROOT/devops/ci-local/.env.local" if [[ -f "$env_file" ]]; then docker_args+=(--env-file "$env_file") fi # Connect to CI network if services are running if docker network inspect stellaops-ci-net &>/dev/null; then docker_args+=(--network stellaops-ci-net) fi log_debug "Running in CI container: $command" docker run "${docker_args[@]}" "$CI_IMAGE" bash -c "$command" } # ============================================================================= # DOCKER NETWORK UTILITIES # ============================================================================= # Get the IP address of a running container get_container_ip() { local container="$1" docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container" 2>/dev/null } # Check if container is running is_container_running() { local container="$1" [[ "$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)" == "true" ]] } # Get container logs get_container_logs() { local container="$1" local lines="${2:-100}" docker logs --tail "$lines" "$container" 2>&1 }