From 89026e3460652e1877a3ee369242c3c4d868f5d4 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Tue, 22 Jul 2025 23:19:17 -0300 Subject: [PATCH] feat: release build --- .claude/settings.local.json | 3 +- .gitea/workflows/release-deploy.yml | 166 ++++++++ BCards.sln | 10 + Dockerfile.release | 123 ++++++ docker-compose.staging.yml | 160 ++++++++ scripts/deploy-release.sh | 369 ++++++++++++++++++ scripts/test-mongodb-connection.sh | 495 ++++++++++++++++++++++++ src/BCards.Web/appsettings.Release.json | 141 +++++++ 8 files changed, 1466 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/release-deploy.yml create mode 100644 Dockerfile.release create mode 100644 docker-compose.staging.yml create mode 100644 scripts/deploy-release.sh create mode 100644 scripts/test-mongodb-connection.sh create mode 100644 src/BCards.Web/appsettings.Release.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c5206da..2ed84fa 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,8 @@ "Bash(rm:*)", "Bash(curl:*)", "Bash(docker-compose up:*)", - "Bash(dotnet build:*)" + "Bash(dotnet build:*)", + "Bash(chmod:*)" ] }, "enableAllProjectMcpServers": false diff --git a/.gitea/workflows/release-deploy.yml b/.gitea/workflows/release-deploy.yml new file mode 100644 index 0000000..e5a951a --- /dev/null +++ b/.gitea/workflows/release-deploy.yml @@ -0,0 +1,166 @@ +name: Release Deployment Pipeline + +on: + push: + branches: + - 'Release/*' + +env: + REGISTRY: registry.redecarneir.us + IMAGE_NAME: bcards + MONGODB_HOST: 192.168.0.100:27017 + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build solution + run: dotnet build --no-restore --configuration Release + + - name: Run unit tests + run: dotnet test --no-build --configuration Release --verbosity normal --collect:"XPlat Code Coverage" + + - name: Test MongoDB connection + run: | + echo "Testing MongoDB connection to $MONGODB_HOST" + timeout 10 bash -c "> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "commit=$COMMIT_SHA" >> $GITHUB_OUTPUT + echo "tag=$VERSION-$COMMIT_SHA" >> $GITHUB_OUTPUT + + - name: Build and push multi-arch Docker image + run: | + echo "Building multi-arch image for platforms: linux/amd64,linux/arm64" + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --file Dockerfile.release \ + --tag $REGISTRY/$IMAGE_NAME:${{ steps.extract_branch.outputs.tag }} \ + --tag $REGISTRY/$IMAGE_NAME:${{ steps.extract_branch.outputs.version }}-latest \ + --tag $REGISTRY/$IMAGE_NAME:release-latest \ + --push \ + --build-arg BUILDPLATFORM=linux/amd64 \ + --build-arg VERSION=${{ steps.extract_branch.outputs.version }} \ + --build-arg COMMIT=${{ steps.extract_branch.outputs.commit }} \ + . + + - name: Deploy to staging environment + run: | + echo "Deploying to staging environment..." + + # Create deployment directory + sudo mkdir -p /opt/bcards-staging + + # Copy docker-compose file + sudo cp docker-compose.staging.yml /opt/bcards-staging/ + + # Set environment variables + sudo tee /opt/bcards-staging/.env > /dev/null < /dev/null 2>&1; then + echo "Application health check passed" + break + fi + echo "Health check attempt $i failed, retrying in 10 seconds..." + sleep 10 + if [ $i -eq 10 ]; then + echo "Health check failed after 10 attempts" + exit 1 + fi + done + + # Test MongoDB connectivity from application + chmod +x scripts/test-mongodb-connection.sh + ./scripts/test-mongodb-connection.sh + + - name: Deployment notification + run: | + echo "🚀 Deployment Status: SUCCESS" + echo "📦 Image: $REGISTRY/$IMAGE_NAME:${{ steps.extract_branch.outputs.tag }}" + echo "🌐 Environment: Staging" + echo "🔗 MongoDB: $MONGODB_HOST" + echo "🏗️ Architecture: Multi-arch (linux/amd64, linux/arm64)" + echo "📋 Branch: ${{ steps.extract_branch.outputs.branch }}" + echo "🆔 Commit: ${{ steps.extract_branch.outputs.commit }}" + + rollback: + name: Rollback on Failure + runs-on: ubuntu-latest + needs: [test, build-and-deploy] + if: failure() + + steps: + - name: Rollback deployment + run: | + echo "🚨 Deployment failed, initiating rollback..." + + # Stop current containers + cd /opt/bcards-staging + sudo docker-compose down + + # Restore previous version if exists + if [ -f .env.backup ]; then + sudo mv .env.backup .env + sudo docker-compose up -d + echo "✅ Rollback completed to previous version" + else + echo "❌ No previous version found for rollback" + fi + + - name: Failure notification + run: | + echo "❌ Deployment Status: FAILED" + echo "🔄 Rollback: Initiated" + echo "📋 Branch: ${GITHUB_REF#refs/heads/}" + echo "🆔 Commit: ${GITHUB_SHA::7}" \ No newline at end of file diff --git a/BCards.sln b/BCards.sln index 70adb9c..095b5c4 100644 --- a/BCards.sln +++ b/BCards.sln @@ -6,6 +6,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCards.Web", "src\BCards.We EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCards.IntegrationTests", "src\BCards.IntegrationTests\BCards.IntegrationTests.csproj", "{8F9E4C7D-2A3B-4E5F-9C8D-1B2A3E4F5C6D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + scripts\deploy-release.sh = scripts\deploy-release.sh + scripts\init-mongo.js = scripts\init-mongo.js + scripts\test-mongodb-connection.sh = scripts\test-mongodb-connection.sh + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,4 +31,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3DA87F09-8B78-450D-9EF8-A0C0E02F0E04} + EndGlobalSection EndGlobal diff --git a/Dockerfile.release b/Dockerfile.release new file mode 100644 index 0000000..32e5e69 --- /dev/null +++ b/Dockerfile.release @@ -0,0 +1,123 @@ +# Dockerfile.release - Multi-architecture build for Release environment +# Supports: linux/amd64, linux/arm64 + +ARG BUILDPLATFORM=linux/amd64 +ARG TARGETPLATFORM +ARG VERSION=unknown +ARG COMMIT=unknown + +# Base runtime image with multi-arch support +FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 +EXPOSE 8443 + +# Install dependencies based on target platform +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libgdiplus \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Create application directories +RUN mkdir -p /app/uploads /app/logs \ + && chmod 755 /app/uploads /app/logs + +# Build stage - use build platform for compilation +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG TARGETPLATFORM +ARG VERSION +ARG COMMIT + +WORKDIR /src + +# Copy project files and restore dependencies +COPY ["src/BCards.Web/BCards.Web.csproj", "src/BCards.Web/"] +RUN dotnet restore "src/BCards.Web/BCards.Web.csproj" \ + --runtime $(echo $TARGETPLATFORM | tr '/' '-') + +# Copy source code +COPY . . +WORKDIR "/src/src/BCards.Web" + +# Build application with Release configuration +RUN dotnet build "BCards.Web.csproj" \ + -c Release \ + -o /app/build \ + --no-restore \ + --runtime $(echo $TARGETPLATFORM | tr '/' '-') \ + -p:Version=$VERSION \ + -p:InformationalVersion=$COMMIT + +# Publish stage - optimize for target platform +FROM build AS publish +ARG TARGETPLATFORM +ARG VERSION +ARG COMMIT + +RUN dotnet publish "BCards.Web.csproj" \ + -c Release \ + -o /app/publish \ + --no-restore \ + --no-build \ + --runtime $(echo $TARGETPLATFORM | tr '/' '-') \ + --self-contained false \ + -p:PublishReadyToRun=true \ + -p:PublishSingleFile=false \ + -p:UseAppHost=false \ + -p:Version=$VERSION \ + -p:InformationalVersion=$COMMIT + +# Final stage - runtime optimized for Release environment +FROM base AS final +ARG VERSION=unknown +ARG COMMIT=unknown +ARG TARGETPLATFORM + +# Metadata labels +LABEL maintainer="BCards Team" +LABEL version=$VERSION +LABEL commit=$COMMIT +LABEL platform=$TARGETPLATFORM +LABEL environment="release" + +WORKDIR /app + +# Copy published application +COPY --from=publish /app/publish . + +# Create non-root user for security +RUN groupadd -r bcards && useradd -r -g bcards bcards \ + && chown -R bcards:bcards /app \ + && chmod +x /app/BCards.Web.dll + +# Environment variables for Release +ENV ASPNETCORE_ENVIRONMENT=Release +ENV ASPNETCORE_URLS=http://+:8080 +ENV DOTNET_RUNNING_IN_CONTAINER=true +ENV DOTNET_EnableDiagnostics=0 +ENV DOTNET_USE_POLLING_FILE_WATCHER=true +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false + +# ARM64 specific optimizations +RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + echo "Applying ARM64 optimizations..." && \ + export DOTNET_TieredPGO=1 && \ + export DOTNET_TC_QuickJitForLoops=1 && \ + export DOTNET_ReadyToRun=0; \ + fi + +# Health check endpoint +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +# Switch to non-root user +USER bcards + +# Entry point with optimized runtime settings +ENTRYPOINT ["dotnet", "BCards.Web.dll"] + +# Runtime configuration for better performance +CMD ["--urls", "http://0.0.0.0:8080"] \ No newline at end of file diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 0000000..e3e7d07 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,160 @@ +version: '3.8' + +services: + bcards-web: + image: ${REGISTRY:-registry.redecarneir.us}/bcards:${IMAGE_TAG:-release-latest} + container_name: bcards-staging + restart: unless-stopped + ports: + - "8090:8080" + - "8453:8443" + environment: + # Core ASP.NET Configuration + - ASPNETCORE_ENVIRONMENT=Release + - ASPNETCORE_URLS=http://+:8080 + - ASPNETCORE_FORWARDEDHEADERS_ENABLED=true + + # MongoDB Configuration + - MongoDb__ConnectionString=${MONGODB_CONNECTION_STRING:-mongodb://192.168.0.100:27017/BCardsDB} + - MongoDb__DatabaseName=BCardsDB + + # Application Settings + - AppSettings__Environment=Staging + - AppSettings__Version=${IMAGE_TAG:-unknown} + - AppSettings__AllowedHosts=* + + # Logging Configuration + - Logging__LogLevel__Default=Information + - Logging__LogLevel__Microsoft.AspNetCore=Warning + - Logging__LogLevel__BCards=Debug + + # Performance Optimizations + - DOTNET_RUNNING_IN_CONTAINER=true + - DOTNET_EnableDiagnostics=0 + - DOTNET_USE_POLLING_FILE_WATCHER=true + - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false + - DOTNET_TieredPGO=1 + - DOTNET_TC_QuickJitForLoops=1 + + # Security Headers + - ASPNETCORE_HTTPS_PORT=8443 + - ASPNETCORE_Kestrel__Certificates__Default__Path=/app/certs/cert.pfx + - ASPNETCORE_Kestrel__Certificates__Default__Password=${CERT_PASSWORD:-} + + # Redis Configuration (if needed) + - Redis__ConnectionString=localhost:6379 + + volumes: + # Application logs + - ./logs:/app/logs:rw + + # File uploads (if needed) + - ./uploads:/app/uploads:rw + + # SSL certificates (if using HTTPS) + # - ./certs:/app/certs:ro + + networks: + - bcards-staging-network + + # Health check configuration + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + # Resource limits for staging environment + deploy: + resources: + limits: + memory: 1G + cpus: '1.0' + reservations: + memory: 512M + cpus: '0.5' + + # Logging configuration + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + + # Platform specification (will use the appropriate arch from multi-arch image) + # platform: linux/amd64 # Uncomment if forcing specific architecture + + # Security options + security_opt: + - no-new-privileges:true + read_only: false # Set to true for extra security, but may need volume mounts for temp files + + # Process limits + ulimits: + nproc: 65535 + nofile: + soft: 65535 + hard: 65535 + + # Optional: Redis for caching (if application uses it) + redis: + image: redis:7-alpine + container_name: bcards-redis-staging + restart: unless-stopped + ports: + - "6379:6379" + command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru + volumes: + - redis_staging_data:/data + networks: + - bcards-staging-network + deploy: + resources: + limits: + memory: 256M + cpus: '0.5' + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + + # Optional: Nginx reverse proxy for additional features + nginx: + image: nginx:alpine + container_name: bcards-nginx-staging + restart: unless-stopped + ports: + - "8091:80" + - "8454:443" + volumes: + - ./nginx/staging.conf:/etc/nginx/conf.d/default.conf:ro + - ./nginx/ssl:/etc/ssl/certs:ro + - ./logs/nginx:/var/log/nginx:rw + depends_on: + - bcards-web + networks: + - bcards-staging-network + deploy: + resources: + limits: + memory: 128M + cpus: '0.25' + +# Named volumes for persistent data +volumes: + redis_staging_data: + driver: local + driver_opts: + type: none + o: bind + device: ./data/redis + +# Network for staging environment +networks: + bcards-staging-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 \ No newline at end of file diff --git a/scripts/deploy-release.sh b/scripts/deploy-release.sh new file mode 100644 index 0000000..b0b052e --- /dev/null +++ b/scripts/deploy-release.sh @@ -0,0 +1,369 @@ +#!/bin/bash + +# Deploy script for Release environment with multi-architecture support +# Usage: ./deploy-release.sh + +set -euo pipefail + +# Configuration +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +readonly DEPLOY_DIR="/opt/bcards-staging" +readonly DOCKER_COMPOSE_FILE="docker-compose.staging.yml" +readonly CONTAINER_NAME="bcards-staging" +readonly HEALTH_CHECK_URL="http://localhost:8090/health" +readonly MAX_HEALTH_CHECK_ATTEMPTS=10 +readonly HEALTH_CHECK_INTERVAL=10 +readonly ROLLBACK_TIMEOUT=300 + +# Colors for output +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Cleanup function +cleanup() { + local exit_code=$? + if [ $exit_code -ne 0 ]; then + log_error "Deployment failed with exit code $exit_code" + rollback_deployment + fi + exit $exit_code +} + +# Set trap for cleanup on exit +trap cleanup EXIT + +# Validate input parameters +validate_input() { + if [ $# -ne 1 ]; then + log_error "Usage: $0 " + exit 1 + fi + + local image_tag="$1" + if [[ ! "$image_tag" =~ ^[a-zA-Z0-9._-]+$ ]]; then + log_error "Invalid image tag format: $image_tag" + exit 1 + fi +} + +# Check prerequisites +check_prerequisites() { + log_info "Checking prerequisites..." + + # Check if Docker is running + if ! docker info >/dev/null 2>&1; then + log_error "Docker is not running or not accessible" + exit 1 + fi + + # Check if docker-compose is available + if ! command -v docker-compose >/dev/null 2>&1; then + log_error "docker-compose is not installed" + exit 1 + fi + + # Check if deployment directory exists + if [ ! -d "$DEPLOY_DIR" ]; then + log_info "Creating deployment directory: $DEPLOY_DIR" + mkdir -p "$DEPLOY_DIR" + fi + + log_success "Prerequisites check passed" +} + +# Backup current deployment +backup_current_deployment() { + log_info "Backing up current deployment..." + + local timestamp=$(date +%Y%m%d_%H%M%S) + local backup_dir="$DEPLOY_DIR/backups/$timestamp" + + mkdir -p "$backup_dir" + + # Backup environment file if exists + if [ -f "$DEPLOY_DIR/.env" ]; then + cp "$DEPLOY_DIR/.env" "$backup_dir/.env.backup" + cp "$DEPLOY_DIR/.env" "$DEPLOY_DIR/.env.backup" + log_info "Environment file backed up" + fi + + # Backup docker-compose file if exists + if [ -f "$DEPLOY_DIR/$DOCKER_COMPOSE_FILE" ]; then + cp "$DEPLOY_DIR/$DOCKER_COMPOSE_FILE" "$backup_dir/${DOCKER_COMPOSE_FILE}.backup" + log_info "Docker compose file backed up" + fi + + # Get current container image for potential rollback + if docker ps --format "table {{.Names}}\t{{.Image}}" | grep -q "$CONTAINER_NAME"; then + local current_image=$(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo "") + if [ -n "$current_image" ]; then + echo "$current_image" > "$DEPLOY_DIR/.previous_image" + log_info "Current image backed up: $current_image" + fi + fi + + log_success "Backup completed: $backup_dir" +} + +# Test MongoDB connectivity +test_mongodb_connection() { + log_info "Testing MongoDB connectivity..." + + local mongodb_host="192.168.0.100" + local mongodb_port="27017" + + # Test basic connectivity + if timeout 10 bash -c "/dev/null; then + log_success "MongoDB connection test passed" + else + log_error "Cannot connect to MongoDB at $mongodb_host:$mongodb_port" + return 1 + fi + + # Run detailed MongoDB test script if available + if [ -f "$SCRIPT_DIR/test-mongodb-connection.sh" ]; then + log_info "Running detailed MongoDB connection tests..." + bash "$SCRIPT_DIR/test-mongodb-connection.sh" + fi +} + +# Pull new Docker image +pull_docker_image() { + local image_tag="$1" + local full_image="registry.redecarneir.us/bcards:$image_tag" + + log_info "Pulling Docker image: $full_image" + + # Pull the multi-arch image + if docker pull "$full_image"; then + log_success "Image pulled successfully" + else + log_error "Failed to pull image: $full_image" + return 1 + fi + + # Verify image architecture + local image_arch=$(docker inspect --format='{{.Architecture}}' "$full_image" 2>/dev/null || echo "unknown") + local system_arch=$(uname -m) + + log_info "Image architecture: $image_arch" + log_info "System architecture: $system_arch" + + # Convert system arch format to Docker format for comparison + case "$system_arch" in + x86_64) system_arch="amd64" ;; + aarch64) system_arch="arm64" ;; + esac + + if [ "$image_arch" = "$system_arch" ] || [ "$image_arch" = "unknown" ]; then + log_success "Image architecture is compatible" + else + log_warning "Image architecture ($image_arch) may not match system ($system_arch), but multi-arch support should handle this" + fi +} + +# Deploy new version +deploy_new_version() { + local image_tag="$1" + + log_info "Deploying new version with tag: $image_tag" + + # Copy docker-compose file to deployment directory + cp "$PROJECT_ROOT/$DOCKER_COMPOSE_FILE" "$DEPLOY_DIR/" + + # Create/update environment file + cat > "$DEPLOY_DIR/.env" << EOF +IMAGE_TAG=$image_tag +REGISTRY=registry.redecarneir.us +MONGODB_CONNECTION_STRING=mongodb://192.168.0.100:27017/BCardsDB +ASPNETCORE_ENVIRONMENT=Release +CERT_PASSWORD= +EOF + + # Stop existing containers + cd "$DEPLOY_DIR" + if docker-compose -f "$DOCKER_COMPOSE_FILE" ps -q | grep -q .; then + log_info "Stopping existing containers..." + docker-compose -f "$DOCKER_COMPOSE_FILE" down --remove-orphans + fi + + # Start new containers + log_info "Starting new containers..." + docker-compose -f "$DOCKER_COMPOSE_FILE" up -d + + # Wait for containers to start + sleep 15 + + log_success "New version deployed" +} + +# Health check +perform_health_check() { + log_info "Performing health check..." + + local attempt=1 + while [ $attempt -le $MAX_HEALTH_CHECK_ATTEMPTS ]; do + log_info "Health check attempt $attempt/$MAX_HEALTH_CHECK_ATTEMPTS" + + # Check if container is running + if ! docker ps --format "table {{.Names}}" | grep -q "$CONTAINER_NAME"; then + log_warning "Container $CONTAINER_NAME is not running" + else + # Check application health endpoint + if curl -f -s "$HEALTH_CHECK_URL" >/dev/null 2>&1; then + log_success "Health check passed" + return 0 + fi + + # Check if the application is responding on port 80 + if curl -f -s "http://localhost:8090/" >/dev/null 2>&1; then + log_success "Application is responding (health endpoint may not be configured)" + return 0 + fi + fi + + if [ $attempt -eq $MAX_HEALTH_CHECK_ATTEMPTS ]; then + log_error "Health check failed after $MAX_HEALTH_CHECK_ATTEMPTS attempts" + return 1 + fi + + log_info "Waiting $HEALTH_CHECK_INTERVAL seconds before next attempt..." + sleep $HEALTH_CHECK_INTERVAL + ((attempt++)) + done +} + +# Rollback deployment +rollback_deployment() { + log_warning "Initiating rollback..." + + cd "$DEPLOY_DIR" + + # Stop current containers + if [ -f "$DOCKER_COMPOSE_FILE" ]; then + docker-compose -f "$DOCKER_COMPOSE_FILE" down --remove-orphans + fi + + # Restore previous environment if backup exists + if [ -f ".env.backup" ]; then + mv ".env.backup" ".env" + log_info "Previous environment restored" + fi + + # Try to start previous version if image is available + if [ -f ".previous_image" ]; then + local previous_image=$(cat ".previous_image") + log_info "Attempting to restore previous image: $previous_image" + + # Update .env with previous image tag + local previous_tag=$(echo "$previous_image" | cut -d':' -f2) + sed -i "s/IMAGE_TAG=.*/IMAGE_TAG=$previous_tag/" .env 2>/dev/null || true + + # Try to start previous version + if docker-compose -f "$DOCKER_COMPOSE_FILE" up -d; then + log_success "Rollback completed successfully" + else + log_error "Rollback failed - manual intervention required" + fi + else + log_warning "No previous version found for rollback" + fi +} + +# Cleanup old images and containers +cleanup_old_resources() { + log_info "Cleaning up old Docker resources..." + + # Remove dangling images + if docker images -f "dangling=true" -q | head -1 | grep -q .; then + docker rmi $(docker images -f "dangling=true" -q) || true + log_info "Dangling images removed" + fi + + # Remove old backups (keep last 5) + if [ -d "$DEPLOY_DIR/backups" ]; then + find "$DEPLOY_DIR/backups" -maxdepth 1 -type d -name "20*" | sort -r | tail -n +6 | xargs rm -rf || true + log_info "Old backups cleaned up" + fi + + log_success "Cleanup completed" +} + +# Display deployment summary +display_summary() { + local image_tag="$1" + + log_success "Deployment Summary:" + echo "==================================" + echo "🚀 Image Tag: $image_tag" + echo "🌐 Environment: Release (Staging)" + echo "🔗 Application URL: http://localhost:8090" + echo "🔗 Health Check: $HEALTH_CHECK_URL" + echo "🗄️ MongoDB: 192.168.0.100:27017" + echo "📁 Deploy Directory: $DEPLOY_DIR" + echo "🐳 Container: $CONTAINER_NAME" + + # Show container status + echo "" + echo "Container Status:" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(NAMES|$CONTAINER_NAME)" || true + + # Show image info + echo "" + echo "Image Information:" + docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.CreatedAt}}\t{{.Size}}" | grep -E "(REPOSITORY|bcards)" | head -5 || true + + echo "==================================" +} + +# Main deployment function +main() { + local image_tag="$1" + + log_info "Starting deployment process for BCards Release environment" + log_info "Target image tag: $image_tag" + log_info "Target architecture: $(uname -m)" + log_info "Deploy directory: $DEPLOY_DIR" + + # Execute deployment steps + validate_input "$@" + check_prerequisites + test_mongodb_connection + backup_current_deployment + pull_docker_image "$image_tag" + deploy_new_version "$image_tag" + + # Perform health check (rollback handled by trap if this fails) + if perform_health_check; then + cleanup_old_resources + display_summary "$image_tag" + log_success "Deployment completed successfully!" + else + log_error "Health check failed - rollback will be triggered" + exit 1 + fi +} + +# Run main function with all arguments +main "$@" \ No newline at end of file diff --git a/scripts/test-mongodb-connection.sh b/scripts/test-mongodb-connection.sh new file mode 100644 index 0000000..c1a922e --- /dev/null +++ b/scripts/test-mongodb-connection.sh @@ -0,0 +1,495 @@ +#!/bin/bash + +# MongoDB Connection Test Script for Release Environment +# Tests connectivity, database operations, and index validation + +set -euo pipefail + +# Configuration +readonly MONGODB_HOST="${MONGODB_HOST:-192.168.0.100}" +readonly MONGODB_PORT="${MONGODB_PORT:-27017}" +readonly DATABASE_NAME="${DATABASE_NAME:-BCardsDB}" +readonly CONNECTION_STRING="mongodb://${MONGODB_HOST}:${MONGODB_PORT}/${DATABASE_NAME}" +readonly TIMEOUT=30 + +# Colors for output +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Test basic TCP connectivity +test_tcp_connection() { + log_info "Testing TCP connection to $MONGODB_HOST:$MONGODB_PORT..." + + if timeout $TIMEOUT bash -c "/dev/null; then + log_success "TCP connection successful" + return 0 + else + log_error "TCP connection failed" + return 1 + fi +} + +# Test MongoDB connectivity using mongosh if available +test_mongodb_with_mongosh() { + if ! command -v mongosh >/dev/null 2>&1; then + log_warning "mongosh not available, skipping MongoDB shell tests" + return 1 + fi + + log_info "Testing MongoDB connection with mongosh..." + + # Test basic connection + local test_output=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval "db.runCommand({ping: 1})" 2>/dev/null || echo "FAILED") + + if [[ "$test_output" == *"ok"* ]]; then + log_success "MongoDB ping successful" + else + log_error "MongoDB ping failed" + return 1 + fi + + # Test database access + log_info "Testing database operations..." + + local db_test=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval " + try { + // Test basic database operations + db.connection_test.insertOne({test: true, timestamp: new Date()}); + var result = db.connection_test.findOne({test: true}); + db.connection_test.deleteOne({test: true}); + print('DATABASE_ACCESS_OK'); + } catch (e) { + print('DATABASE_ACCESS_FAILED: ' + e.message); + } + " 2>/dev/null || echo "DATABASE_ACCESS_FAILED") + + if [[ "$db_test" == *"DATABASE_ACCESS_OK"* ]]; then + log_success "Database operations test passed" + else + log_error "Database operations test failed: $db_test" + return 1 + fi + + return 0 +} + +# Test MongoDB connectivity using Python if available +test_mongodb_with_python() { + if ! command -v python3 >/dev/null 2>&1; then + log_warning "Python3 not available, skipping Python MongoDB tests" + return 1 + fi + + log_info "Testing MongoDB connection with Python..." + + python3 << EOF +import sys +try: + import pymongo + from pymongo import MongoClient + import socket + + # Test connection + client = MongoClient("$CONNECTION_STRING", serverSelectionTimeoutMS=$((TIMEOUT * 1000))) + + # Test ping + client.admin.command('ping') + print("MongoDB ping successful (Python)") + + # Test database access + db = client["$DATABASE_NAME"] + + # Insert test document + test_collection = db.connection_test + result = test_collection.insert_one({"test": True, "source": "python"}) + + # Read test document + doc = test_collection.find_one({"_id": result.inserted_id}) + if doc: + print("Database read/write test passed (Python)") + + # Cleanup + test_collection.delete_one({"_id": result.inserted_id}) + + client.close() + print("PYTHON_TEST_SUCCESS") + +except ImportError: + print("PyMongo not installed, skipping Python tests") + sys.exit(1) +except Exception as e: + print(f"Python MongoDB test failed: {e}") + sys.exit(1) +EOF + + local python_result=$? + if [ $python_result -eq 0 ]; then + log_success "Python MongoDB test passed" + return 0 + else + log_error "Python MongoDB test failed" + return 1 + fi +} + +# Test using Docker MongoDB client +test_mongodb_with_docker() { + if ! command -v docker >/dev/null 2>&1; then + log_warning "Docker not available, skipping Docker MongoDB tests" + return 1 + fi + + log_info "Testing MongoDB connection using Docker MongoDB client..." + + # Use official MongoDB image to test connection + local docker_test=$(timeout $TIMEOUT docker run --rm mongo:7.0 mongosh "$CONNECTION_STRING" --quiet --eval " + try { + db.runCommand({ping: 1}); + db.connection_test.insertOne({test: true, source: 'docker', timestamp: new Date()}); + var doc = db.connection_test.findOne({source: 'docker'}); + db.connection_test.deleteOne({source: 'docker'}); + print('DOCKER_TEST_SUCCESS'); + } catch (e) { + print('DOCKER_TEST_FAILED: ' + e.message); + } + " 2>/dev/null || echo "DOCKER_TEST_FAILED") + + if [[ "$docker_test" == *"DOCKER_TEST_SUCCESS"* ]]; then + log_success "Docker MongoDB test passed" + return 0 + else + log_error "Docker MongoDB test failed: $docker_test" + return 1 + fi +} + +# Test MongoDB from application container +test_from_application_container() { + local container_name="bcards-staging" + + if ! docker ps --format "{{.Names}}" | grep -q "^${container_name}$"; then + log_warning "Application container '$container_name' not running, skipping application test" + return 1 + fi + + log_info "Testing MongoDB connection from application container..." + + # Test connection from the application container + local app_test=$(docker exec "$container_name" timeout 10 bash -c " + # Test TCP connection + if timeout 5 bash -c '/dev/null; then + echo 'APP_TCP_OK' + else + echo 'APP_TCP_FAILED' + exit 1 + fi + + # Test HTTP health endpoint if available + if curl -f -s http://localhost:8080/health >/dev/null 2>&1; then + echo 'APP_HEALTH_OK' + else + echo 'APP_HEALTH_FAILED' + fi + " 2>/dev/null || echo "APP_TEST_FAILED") + + if [[ "$app_test" == *"APP_TCP_OK"* ]]; then + log_success "Application container can connect to MongoDB" + + if [[ "$app_test" == *"APP_HEALTH_OK"* ]]; then + log_success "Application health check passed" + else + log_warning "Application health check failed - app may still be starting" + fi + return 0 + else + log_error "Application container cannot connect to MongoDB" + return 1 + fi +} + +# Check MongoDB server status and version +check_mongodb_status() { + log_info "Checking MongoDB server status..." + + # Try multiple methods to check status + local status_checked=false + + # Method 1: Using mongosh + if command -v mongosh >/dev/null 2>&1; then + local server_status=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval " + try { + var status = db.runCommand({serverStatus: 1}); + print('MongoDB Version: ' + status.version); + print('Uptime: ' + status.uptime + ' seconds'); + print('Connections: ' + status.connections.current + '/' + status.connections.available); + print('STATUS_CHECK_OK'); + } catch (e) { + print('STATUS_CHECK_FAILED: ' + e.message); + } + " 2>/dev/null || echo "STATUS_CHECK_FAILED") + + if [[ "$server_status" == *"STATUS_CHECK_OK"* ]]; then + echo "$server_status" | grep -v "STATUS_CHECK_OK" + log_success "MongoDB server status check passed" + status_checked=true + fi + fi + + # Method 2: Using Docker if mongosh failed + if [ "$status_checked" = false ] && command -v docker >/dev/null 2>&1; then + local docker_status=$(timeout $TIMEOUT docker run --rm mongo:7.0 mongosh "$CONNECTION_STRING" --quiet --eval " + try { + var status = db.runCommand({serverStatus: 1}); + print('MongoDB Version: ' + status.version); + print('STATUS_CHECK_OK'); + } catch (e) { + print('STATUS_CHECK_FAILED: ' + e.message); + } + " 2>/dev/null || echo "STATUS_CHECK_FAILED") + + if [[ "$docker_status" == *"STATUS_CHECK_OK"* ]]; then + echo "$docker_status" | grep -v "STATUS_CHECK_OK" + log_success "MongoDB server status check passed (via Docker)" + status_checked=true + fi + fi + + if [ "$status_checked" = false ]; then + log_warning "Could not retrieve MongoDB server status" + return 1 + fi + + return 0 +} + +# Test BCards specific collections and indexes +test_bcards_collections() { + if ! command -v mongosh >/dev/null 2>&1 && ! command -v docker >/dev/null 2>&1; then + log_warning "Cannot test BCards collections - no MongoDB client available" + return 1 + fi + + log_info "Testing BCards specific collections and indexes..." + + local mongo_cmd="mongosh" + local docker_prefix="" + + if ! command -v mongosh >/dev/null 2>&1; then + mongo_cmd="docker run --rm mongo:7.0 mongosh" + docker_prefix="timeout $TIMEOUT " + fi + + local collections_test=$(${docker_prefix}${mongo_cmd} "$CONNECTION_STRING" --quiet --eval " + try { + // Check required collections + var collections = db.listCollectionNames(); + var requiredCollections = ['users', 'userpages', 'categories']; + var missingCollections = []; + + requiredCollections.forEach(function(collection) { + if (collections.indexOf(collection) === -1) { + missingCollections.push(collection); + } + }); + + if (missingCollections.length > 0) { + print('Missing collections: ' + missingCollections.join(', ')); + } else { + print('All required collections exist'); + } + + // Check indexes on userpages collection + if (collections.indexOf('userpages') !== -1) { + var indexes = db.userpages.getIndexes(); + print('UserPages collection has ' + indexes.length + ' indexes'); + + // Check for important compound index + var hasCompoundIndex = indexes.some(function(index) { + return index.key && index.key.category && index.key.slug; + }); + + if (hasCompoundIndex) { + print('Required compound index (category, slug) exists'); + } else { + print('WARNING: Compound index (category, slug) is missing'); + } + } + + print('COLLECTIONS_TEST_OK'); + } catch (e) { + print('COLLECTIONS_TEST_FAILED: ' + e.message); + } + " 2>/dev/null || echo "COLLECTIONS_TEST_FAILED") + + if [[ "$collections_test" == *"COLLECTIONS_TEST_OK"* ]]; then + echo "$collections_test" | grep -v "COLLECTIONS_TEST_OK" + log_success "BCards collections test passed" + return 0 + else + log_warning "BCards collections test had issues: $collections_test" + return 1 + fi +} + +# Performance test +test_mongodb_performance() { + log_info "Running basic performance test..." + + if ! command -v mongosh >/dev/null 2>&1; then + log_warning "mongosh not available, skipping performance test" + return 1 + fi + + local perf_test=$(timeout $TIMEOUT mongosh "$CONNECTION_STRING" --quiet --eval " + try { + var start = new Date(); + + // Insert test documents + var docs = []; + for (var i = 0; i < 100; i++) { + docs.push({test: true, index: i, timestamp: new Date()}); + } + db.performance_test.insertMany(docs); + + // Read test + var count = db.performance_test.countDocuments({test: true}); + + // Update test + db.performance_test.updateMany({test: true}, {\$set: {updated: true}}); + + // Delete test + db.performance_test.deleteMany({test: true}); + + var end = new Date(); + var duration = end - start; + + print('Performance test completed in ' + duration + 'ms'); + print('Operations: 100 inserts, 1 count, 100 updates, 100 deletes'); + + if (duration < 5000) { + print('PERFORMANCE_TEST_OK'); + } else { + print('PERFORMANCE_TEST_SLOW'); + } + } catch (e) { + print('PERFORMANCE_TEST_FAILED: ' + e.message); + } + " 2>/dev/null || echo "PERFORMANCE_TEST_FAILED") + + if [[ "$perf_test" == *"PERFORMANCE_TEST_OK"* ]]; then + echo "$perf_test" | grep -v "PERFORMANCE_TEST_OK" + log_success "Performance test passed" + return 0 + elif [[ "$perf_test" == *"PERFORMANCE_TEST_SLOW"* ]]; then + echo "$perf_test" | grep -v "PERFORMANCE_TEST_SLOW" + log_warning "Performance test completed but was slow" + return 0 + else + log_error "Performance test failed: $perf_test" + return 1 + fi +} + +# Display connection summary +display_summary() { + echo "" + log_info "MongoDB Connection Test Summary" + echo "==================================================" + echo "🏠 Host: $MONGODB_HOST:$MONGODB_PORT" + echo "🗄️ Database: $DATABASE_NAME" + echo "🔗 Connection String: $CONNECTION_STRING" + echo "⏱️ Timeout: ${TIMEOUT}s" + echo "📊 Tests completed: $(date)" + echo "==================================================" +} + +# Main test function +main() { + log_info "Starting MongoDB connection tests for Release environment" + + local test_results=() + local overall_success=true + + # Run all tests + if test_tcp_connection; then + test_results+=("✅ TCP Connection") + else + test_results+=("❌ TCP Connection") + overall_success=false + fi + + if test_mongodb_with_mongosh; then + test_results+=("✅ MongoDB Shell") + elif test_mongodb_with_docker; then + test_results+=("✅ MongoDB Docker") + elif test_mongodb_with_python; then + test_results+=("✅ MongoDB Python") + else + test_results+=("❌ MongoDB Client") + overall_success=false + fi + + if test_from_application_container; then + test_results+=("✅ Application Container") + else + test_results+=("⚠️ Application Container") + fi + + if check_mongodb_status; then + test_results+=("✅ Server Status") + else + test_results+=("⚠️ Server Status") + fi + + if test_bcards_collections; then + test_results+=("✅ BCards Collections") + else + test_results+=("⚠️ BCards Collections") + fi + + if test_mongodb_performance; then + test_results+=("✅ Performance Test") + else + test_results+=("⚠️ Performance Test") + fi + + # Display results + display_summary + echo "" + log_info "Test Results:" + for result in "${test_results[@]}"; do + echo " $result" + done + + echo "" + if [ "$overall_success" = true ]; then + log_success "All critical MongoDB tests passed!" + exit 0 + else + log_error "Some critical MongoDB tests failed!" + exit 1 + fi +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/src/BCards.Web/appsettings.Release.json b/src/BCards.Web/appsettings.Release.json new file mode 100644 index 0000000..f575bc7 --- /dev/null +++ b/src/BCards.Web/appsettings.Release.json @@ -0,0 +1,141 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning", + "BCards": "Information" + }, + "Console": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Information" + } + }, + "File": { + "Path": "/app/logs/bcards-{Date}.log", + "LogLevel": { + "Default": "Information" + } + } + }, + "AllowedHosts": "*", + "MongoDb": { + "ConnectionString": "mongodb://192.168.0.100:27017/BCardsDB", + "DatabaseName": "BCardsDB", + "MaxConnectionPoolSize": 100, + "ConnectTimeout": "30s", + "ServerSelectionTimeout": "30s", + "SocketTimeout": "30s" + }, + "Stripe": { + "PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS", + "SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO", + "WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543", + "ApiVersion": "2023-10-16" + }, + "Authentication": { + "Google": { + "ClientId": "472850008574-nmeepbdt4hunsk5c8krpbdmd3olc4jv6.apps.googleusercontent.com", + "ClientSecret": "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" + }, + "Microsoft": { + "ClientId": "b411606a-e574-4f59-b7cd-10dd941b9fa3", + "ClientSecret": "T0.8Q~an.51iW1H0DVjL2i1bmSK_qTgVQOuEmapK" + }, + "CookieSettings": { + "SecurePolicy": "Always", + "SameSiteMode": "Lax", + "HttpOnly": true, + "ExpireTimeSpan": "7.00:00:00" + } + }, + "Plans": { + "Basic": { + "PriceId": "price_1RjUskBMIadsOxJVgLwlVo1y", + "Price": 9.90, + "MaxLinks": 5, + "Features": [ "basic_themes", "simple_analytics" ] + }, + "Professional": { + "PriceId": "price_1RjUv9BMIadsOxJVORqlM4E9", + "Price": 24.90, + "MaxLinks": 15, + "Features": [ "all_themes", "advanced_analytics", "custom_domain" ] + }, + "Premium": { + "PriceId": "price_1RjUw0BMIadsOxJVmdouNV1g", + "Price": 29.90, + "MaxLinks": -1, + "Features": [ "custom_themes", "full_analytics", "multiple_domains", "priority_support" ] + } + }, + "Moderation": { + "PriorityTimeframes": { + "Trial": "7.00:00:00", + "Basic": "7.00:00:00", + "Professional": "3.00:00:00", + "Premium": "1.00:00:00" + }, + "MaxAttempts": 3, + "ModeratorEmail": "ricardo.carneiro@jobmaker.com.br", + "ModeratorEmails": [ + "rrcgoncalves@gmail.com", + "rirocarneiro@gmail.com" + ], + "PreviewTokenExpirationHours": 4, + "AutoRejectDays": 30 + }, + "SendGrid": { + "ApiKey": "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", + "FromEmail": "ricardo.carneiro@jobmaker.com.br", + "FromName": "BCards - Staging", + "ReplyToEmail": "ricardo.carneiro@jobmaker.com.br" + }, + "BaseUrl": "http://192.168.0.100:8090", + "Environment": { + "Name": "Release", + "IsStagingEnvironment": true, + "AllowTestData": true, + "EnableDetailedErrors": false + }, + "Performance": { + "EnableCaching": true, + "CacheExpirationMinutes": 30, + "EnableCompression": true, + "EnableResponseCaching": true + }, + "Security": { + "EnableHttpsRedirection": false, + "EnableHsts": false, + "RequireHttpsMetadata": false, + "CorsOrigins": [ + "http://192.168.0.100:8090", + "http://localhost:8090" + ] + }, + "HealthChecks": { + "Enabled": true, + "Endpoints": { + "Health": "/health", + "Ready": "/ready", + "Live": "/live" + }, + "MongoDb": { + "Enabled": true, + "Timeout": "10s" + } + }, + "Monitoring": { + "EnableMetrics": true, + "MetricsEndpoint": "/metrics", + "EnableTracing": false + }, + "Features": { + "EnablePreviewMode": true, + "EnableModerationWorkflow": true, + "EnableAnalytics": true, + "EnableFileUploads": true, + "MaxFileUploadSize": "5MB" + } +} \ No newline at end of file