diff --git a/.github/workflows/build-consumer-service.yml b/.github/workflows/build-consumer-service.yml new file mode 100644 index 0000000..bc323b0 --- /dev/null +++ b/.github/workflows/build-consumer-service.yml @@ -0,0 +1,126 @@ +name: Build and Push Consumer Service Docker Image + +on: + push: + branches: + - main + - develop + paths: + - 'consumer-service/**' + - '.github/workflows/build-consumer-service.yml' + pull_request: + branches: + - main + - develop + paths: + - 'consumer-service/**' + workflow_dispatch: + inputs: + version: + description: 'Docker image version' + required: false + default: 'latest' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/consumer-service + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,prefix={{branch}}- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: ./consumer-service + file: ./consumer-service/Dockerfile.prod + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ github.sha }} + BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ') + GIT_COMMIT=${{ github.sha }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Download dependencies + run: | + cd consumer-service + go mod download + + - name: Run tests + run: | + cd consumer-service + go test -v ./... + + - name: Run linter + uses: golangci/golangci-lint-action@v3 + with: + working-directory: consumer-service + version: latest + + scan: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: 'consumer-service' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' diff --git a/consumer-service/.dockerignore b/consumer-service/.dockerignore new file mode 100644 index 0000000..e5afc63 --- /dev/null +++ b/consumer-service/.dockerignore @@ -0,0 +1,64 @@ +# Git files +.git +.gitignore +.gitattributes + +# IDEs and Editors +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store +.env.local +.env.*.local + +# Build and test artifacts +.bin +*.o +*.a +*.so +*.exe +*.test +coverage.out +*.coverprofile + +# Temporary files +tmp/ +temp/ +*.tmp + +# Docker files +Dockerfile +Dockerfile.* +docker-compose*.yml +.dockerignore +.docker + +# CI/CD +.github +.gitlab-ci.yml +.circleci + +# Documentation +*.md +docs/ +README.md + +# Development files +Makefile +makefile +scripts/ +*.sh + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log +logs/ + +# OS files +Thumbs.db diff --git a/consumer-service/Dockerfile b/consumer-service/Dockerfile new file mode 100644 index 0000000..3a4c3ac --- /dev/null +++ b/consumer-service/Dockerfile @@ -0,0 +1,50 @@ +# Stage 1: Builder +FROM golang:1.26-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata openssh-client + +WORKDIR /app + +# Configure Git for private repos +RUN git config --global url."git@github.com:".insteadOf "https://github.com/" + +# Copy SSH key for private repo access (build arg) +ARG SSH_PRIVATE_KEY +RUN mkdir -p /root/.ssh && \ + echo "${SSH_PRIVATE_KEY}" > /root/.ssh/id_rsa && \ + chmod 600 /root/.ssh/id_rsa && \ + ssh-keyscan -H github.com >> /root/.ssh/known_hosts + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o consumer-service . + +# Stage 2: Runtime +FROM alpine:3.18 + +RUN apk --no-cache add ca-certificates tzdata + +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser + +WORKDIR /home/appuser + +COPY --from=builder --chown=appuser:appuser /app/consumer-service . + +USER appuser + +EXPOSE 50051 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD nc -z localhost 50051 || exit 1 + +CMD ["./consumer-service"] \ No newline at end of file diff --git a/consumer-service/Dockerfile.prod b/consumer-service/Dockerfile.prod new file mode 100644 index 0000000..037417e --- /dev/null +++ b/consumer-service/Dockerfile.prod @@ -0,0 +1,60 @@ +# Stage 1: Builder +FROM golang:1.26-alpine AS builder + +ARG VERSION=dev +ARG BUILD_TIME +ARG GIT_COMMIT + +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata make + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies (cached layer) +RUN go mod download + +# Copy source code +COPY . . + +# Build with optimizations +ARG TARGETARCH + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build \ + -a \ + -installsuffix cgo \ + -o consumer-service . + +# Stage 2: Runtime +FROM alpine:3.18 + +# Install minimal runtime dependencies +RUN apk --no-cache add ca-certificates tzdata curl + +# Create non-root user for security +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser + +WORKDIR /home/appuser + +# Copy binary from builder +COPY --from=builder --chown=appuser:appuser /app/consumer-service . + +# Copy any config files if needed +# COPY --from=builder --chown=appuser:appuser /app/config ./config + +USER appuser + +# Expose gRPC port +EXPOSE 50051 + +# Metadata +LABEL org.opencontainers.image.title="HireMind Consumer Service" +LABEL org.opencontainers.image.description="gRPC service for interview management" +LABEL org.opencontainers.image.version="${VERSION}" + + +# Run the application +CMD ["./consumer-service"] diff --git a/consumer-service/Makefile b/consumer-service/Makefile new file mode 100644 index 0000000..3c92a0e --- /dev/null +++ b/consumer-service/Makefile @@ -0,0 +1,115 @@ +.PHONY: help build run stop clean logs compose-up compose-down compose-logs docker-build docker-push version + +# Variables +SERVICE_NAME=consumer-service +DOCKER_REGISTRY=docker.io +DOCKER_IMAGE=$(DOCKER_REGISTRY)/hiremind/$(SERVICE_NAME) +VERSION?=latest +BUILD_TIME=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') +GIT_COMMIT=$(shell git rev-parse --short HEAD) + +help: ## Display this help message + @echo "HireMind Consumer Service - Docker & Development Commands" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +# Docker Commands +build: ## Build Docker image + docker build \ + -t $(DOCKER_IMAGE):$(VERSION) \ + -t $(DOCKER_IMAGE):latest \ + --build-arg VERSION=$(VERSION) \ + --build-arg BUILD_TIME=$(BUILD_TIME) \ + --build-arg GIT_COMMIT=$(GIT_COMMIT) \ + -f Dockerfile . + +build-prod: ## Build production Docker image + docker build \ + -t $(DOCKER_IMAGE):$(VERSION)-prod \ + --build-arg VERSION=$(VERSION) \ + --build-arg BUILD_TIME=$(BUILD_TIME) \ + --build-arg GIT_COMMIT=$(GIT_COMMIT) \ + -f Dockerfile.prod . + +run: ## Run Docker container + docker run -it --rm \ + --env-file .env \ + -p 50051:50051 \ + $(DOCKER_IMAGE):$(VERSION) + +docker-push: ## Push Docker image to registry + docker push $(DOCKER_IMAGE):$(VERSION) + docker push $(DOCKER_IMAGE):latest + +# Docker Compose Commands +compose-up: ## Start services with docker-compose + docker-compose up -d + +compose-down: ## Stop services with docker-compose + docker-compose down + +compose-logs: ## View docker-compose logs + docker-compose logs -f + +compose-logs-service: ## View consumer-service logs only + docker-compose logs -f consumer-service + +compose-logs-db: ## View postgres logs only + docker-compose logs -f postgres + +compose-ps: ## Show running containers + docker-compose ps + +compose-rebuild: ## Rebuild container and start + docker-compose up -d --build + +# Development Commands +dev: ## Run service in development mode + go run main.go + +test: ## Run tests + go test -v ./... + +test-coverage: ## Run tests with coverage + go test -cover ./... -out=coverage.out + go tool cover -html=coverage.out + +lint: ## Run linter + golangci-lint run + +fmt: ## Format code + go fmt ./... + +clean: ## Clean build artifacts + rm -f $(SERVICE_NAME) + docker image prune -f + +# Database Commands +db-migrate-up: ## Run database migrations + docker-compose exec postgres psql -U hiremind_user -d hiremind_db -c "SELECT * FROM migrations;" + +db-shell: ## Open database shell + docker-compose exec postgres psql -U hiremind_user -d hiremind_db + +# Utility Commands +version: ## Display build information + @echo "Service: $(SERVICE_NAME)" + @echo "Version: $(VERSION)" + @echo "Docker Image: $(DOCKER_IMAGE)" + @echo "Build Time: $(BUILD_TIME)" + @echo "Git Commit: $(GIT_COMMIT)" + +health-check: ## Check service health + curl -f http://localhost:50051/health || echo "Service not healthy" + +logs-follow: ## Follow application logs + tail -f logs/app.log + +image-size: ## Show Docker image size + docker images $(DOCKER_IMAGE) + +deps: ## Download Go dependencies + go mod download + go mod tidy + +.DEFAULT_GOAL := help diff --git a/consumer-service/config/config.go b/consumer-service/config/config.go index 154722b..910390e 100644 --- a/consumer-service/config/config.go +++ b/consumer-service/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "log" "os" @@ -11,20 +12,46 @@ type Config struct { DBReadURL string DBWriteURL string GRPCPort string + RedisHost string + RedisAddr string + JWTSecret string + Env string + LogLevel string } -var AppConfig Config - -func LoadConfig() { - - err := godotenv.Load() - if err != nil { - log.Fatal("failed to load env") +func LoadConfig() (*Config, error) { + // Only load .env file if it exists (for local development) + // In Docker, environment variables are set via docker-compose.yml + if _, err := os.Stat(".env"); err == nil { + godotenv.Load() } - AppConfig = Config{ + cfg := &Config{ DBReadURL: os.Getenv("DB_READ_URL"), DBWriteURL: os.Getenv("DB_WRITE_URL"), GRPCPort: os.Getenv("GRPC_PORT"), + RedisHost: os.Getenv("REDIS_HOST"), + RedisAddr: os.Getenv("REDIS_ADDR"), + JWTSecret: os.Getenv("JWT_SECRET"), + Env: os.Getenv("ENV"), + LogLevel: os.Getenv("LOG_LEVEL"), } + + // Validate required fields + if cfg.DBReadURL == "" || cfg.DBWriteURL == "" { + log.Println("failed to load env: missing database URLs") + return nil, errors.New("missing required environment variables") + } + + if cfg.GRPCPort == "" { + log.Println("failed to load env: missing GRPC_PORT") + return nil, errors.New("missing GRPC_PORT") + } + + if cfg.JWTSecret == "" { + log.Println("failed to load env: missing JWT_SECRET") + return nil, errors.New("missing JWT_SECRET") + } + + return cfg, nil } diff --git a/consumer-service/consumer-service b/consumer-service/consumer-service index d678ac0..5bc75eb 100755 Binary files a/consumer-service/consumer-service and b/consumer-service/consumer-service differ diff --git a/consumer-service/docker-compose.dev.yml b/consumer-service/docker-compose.dev.yml new file mode 100644 index 0000000..dc7f1a9 --- /dev/null +++ b/consumer-service/docker-compose.dev.yml @@ -0,0 +1,73 @@ +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: hiremind_postgres_dev + environment: + POSTGRES_USER: hiremind_user + POSTGRES_PASSWORD: hiremind_password + POSTGRES_DB: hiremind_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U hiremind_user" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - hiremind_dev_network + + redis: + image: redis:7-alpine + container_name: hiremind_redis_dev + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - hiremind_dev_network + + consumer-service: + build: + context: . + dockerfile: Dockerfile + container_name: hiremind_consumer_service_dev + environment: + - PORT=50051 + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=hiremind_user + - DB_PASSWORD=hiremind_password + - DB_NAME=hiremind_db + - DB_SSLMODE=disable + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_DB=0 + - LOG_LEVEL=debug + ports: + - "50051:50051" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - hiremind_dev_network + restart: unless-stopped + +volumes: + postgres_data: + redis_data: + + +networks: + hiremind_dev_network: + driver: bridge diff --git a/consumer-service/docker-compose.yml b/consumer-service/docker-compose.yml new file mode 100644 index 0000000..06f81aa --- /dev/null +++ b/consumer-service/docker-compose.yml @@ -0,0 +1,41 @@ +services: + redis: + image: redis:7-alpine + container_name: hiremind-redis + ports: + - "6379:6379" + networks: + - hiremind-network + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + consumer-service: + build: + context: . + dockerfile: Dockerfile + container_name: hiremind-consumer-service + volumes: + - ./.env:/.env:ro + env_file: + - .env + ports: + - "50051:50051" + depends_on: + redis: + condition: service_healthy + networks: + - hiremind-network + restart: unless-stopped + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "50051" ] + interval: 30s + timeout: 3s + retries: 3 + +networks: + hiremind-network: + driver: bridge diff --git a/consumer-service/main.go b/consumer-service/main.go index 03c9598..8117d05 100644 --- a/consumer-service/main.go +++ b/consumer-service/main.go @@ -3,7 +3,6 @@ package main import ( "log" "net" - "os" "consumer-service/config" "consumer-service/db" @@ -23,11 +22,14 @@ import ( func main() { - config.LoadConfig() + cfg, err := config.LoadConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } - err := db.InitDB( - config.AppConfig.DBReadURL, - config.AppConfig.DBWriteURL, + err = db.InitDB( + cfg.DBReadURL, + cfg.DBWriteURL, ) if err != nil { @@ -35,12 +37,12 @@ func main() { } rdb := redis.NewClient(&redis.Options{ - Addr: os.Getenv("REDIS_ADDR"), + Addr: cfg.RedisAddr, }) redisWrapper := redisclient.NewClient(rdb) - jwtSecret := os.Getenv("JWT_SECRET") + jwtSecret := cfg.JWTSecret if jwtSecret == "" { log.Fatal("JWT_SECRET not set") } diff --git a/consumer-service/scripts/docker-build.sh b/consumer-service/scripts/docker-build.sh new file mode 100644 index 0000000..871bd9c --- /dev/null +++ b/consumer-service/scripts/docker-build.sh @@ -0,0 +1,135 @@ +#!/bin/bash +# Docker build script for Consumer Service + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +SERVICE_NAME="consumer-service" +DOCKER_REGISTRY="${DOCKER_REGISTRY:-docker.io}" +DOCKER_IMAGE="${DOCKER_REGISTRY}/hiremind/${SERVICE_NAME}" +VERSION="${VERSION:-latest}" +BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ') +GIT_COMMIT=$(git rev-parse --short HEAD) +DOCKERFILE="${DOCKERFILE:-Dockerfile}" +PUSH="${PUSH:-false}" +NO_CACHE="${NO_CACHE:-false}" + +# Help function +show_help() { + cat << EOF +Usage: $0 [OPTIONS] + +Docker build script for $SERVICE_NAME + +OPTIONS: + -v, --version VERSION Docker image version (default: latest) + -r, --registry REGISTRY Docker registry (default: docker.io) + -f, --file DOCKERFILE Dockerfile to use (default: Dockerfile) + -p, --push Push image to registry after building + --no-cache Build without using cache + -h, --help Show this help message + +EXAMPLES: + $0 # Build with default settings + $0 -v 1.0.0 # Build version 1.0.0 + $0 -v 1.0.0 --push # Build and push version 1.0.0 + $0 -f Dockerfile.prod -p # Build production image and push + +EOF +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -v|--version) + VERSION="$2" + shift 2 + ;; + -r|--registry) + DOCKER_REGISTRY="$2" + DOCKER_IMAGE="${DOCKER_REGISTRY}/hiremind/${SERVICE_NAME}" + shift 2 + ;; + -f|--file) + DOCKERFILE="$2" + shift 2 + ;; + -p|--push) + PUSH="true" + shift + ;; + --no-cache) + NO_CACHE="true" + shift + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + show_help + exit 1 + ;; + esac +done + +# Validate Dockerfile exists +if [ ! -f "$DOCKERFILE" ]; then + echo -e "${RED}Error: Dockerfile not found: $DOCKERFILE${NC}" + exit 1 +fi + +# Build image +echo -e "${YELLOW}Building Docker image...${NC}" +echo "Image: ${DOCKER_IMAGE}:${VERSION}" +echo "Dockerfile: $DOCKERFILE" +echo "Build Time: $BUILD_TIME" +echo "Git Commit: $GIT_COMMIT" +echo "" + +BUILD_ARGS="--build-arg VERSION=${VERSION} --build-arg BUILD_TIME=${BUILD_TIME} --build-arg GIT_COMMIT=${GIT_COMMIT}" + +if [ "$NO_CACHE" = "true" ]; then + BUILD_ARGS="$BUILD_ARGS --no-cache" +fi + +docker build \ + $BUILD_ARGS \ + -t "${DOCKER_IMAGE}:${VERSION}" \ + -t "${DOCKER_IMAGE}:latest" \ + -f "$DOCKERFILE" \ + . + +if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Build successful!${NC}" + + # Display image info + echo "" + echo "Image details:" + docker images "${DOCKER_IMAGE}" + + # Push if requested + if [ "$PUSH" = "true" ]; then + echo "" + echo -e "${YELLOW}Pushing Docker image...${NC}" + docker push "${DOCKER_IMAGE}:${VERSION}" + docker push "${DOCKER_IMAGE}:latest" + + if [ $? -eq 0 ]; then + echo -e "${GREEN}✓ Push successful!${NC}" + else + echo -e "${RED}✗ Push failed!${NC}" + exit 1 + fi + fi +else + echo -e "${RED}✗ Build failed!${NC}" + exit 1 +fi diff --git a/consumer-service/scripts/setup.sh b/consumer-service/scripts/setup.sh new file mode 100644 index 0000000..56ba54b --- /dev/null +++ b/consumer-service/scripts/setup.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Setup script for Docker development environment + +set -e + +# Colors +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}🚀 Setting up Docker development environment...${NC}" + +# Check if Docker is installed +if ! command -v docker &> /dev/null; then + echo "Docker is not installed. Please install Docker Desktop." + exit 1 +fi + +# Copy .env.example to .env if it doesn't exist +if [ ! -f .env ]; then + echo -e "${BLUE}Creating .env file from .env.example...${NC}" + cp .env.example .env + echo -e "${GREEN}✓ .env created${NC}" +else + echo -e "${GREEN}✓ .env already exists${NC}" +fi + +# Make scripts executable +echo -e "${BLUE}Making scripts executable...${NC}" +chmod +x scripts/*.sh + +# Build Docker image +echo -e "${BLUE}Building Docker image...${NC}" +make build + +# Start services +echo -e "${BLUE}Starting services with docker-compose...${NC}" +make compose-up + +# Wait for services to be healthy +echo -e "${BLUE}Waiting for services to be healthy...${NC}" +sleep 5 + +# Check service status +echo -e "${BLUE}Checking service status...${NC}" +docker-compose ps + +echo -e "${GREEN}✓ Setup complete!${NC}" +echo "" +echo "Next steps:" +echo " - View logs: make compose-logs" +echo " - Stop services: make compose-down" +echo " - Run tests: make test" +echo " - View more commands: make help" diff --git a/consumer-service/services/interviewService/utils.go b/consumer-service/services/interviewService/utils.go index 82f42e1..dc333bf 100644 --- a/consumer-service/services/interviewService/utils.go +++ b/consumer-service/services/interviewService/utils.go @@ -21,7 +21,7 @@ func getValidateGetInterviewsRequest(ctx context.Context, req GetInterviewsReque if req.UserID != "" && req.UserID != authenticatedUserID { return GetInterviewsRequest{}, errorv2.BadRequest.New("user_id does not match authenticated user") } - req.UserID = authenticatedUserID + req.UserID = authenticatedUserID // user from request to be fixed if err := common.ValidateUUID(req.UserID); err != nil { return GetInterviewsRequest{}, errorv2.BadRequest.Wrap(err, "invalid user_id") diff --git a/gateway-service/.dockerignore b/gateway-service/.dockerignore new file mode 100644 index 0000000..f418574 --- /dev/null +++ b/gateway-service/.dockerignore @@ -0,0 +1,61 @@ +# Git files +.git +.gitignore +.gitattributes + +# IDEs and Editors +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store +.env +.env.local +.env.*.local + +# Build and test artifacts +.bin +gateway-service +*.o +*.a +*.so +*.exe +*.test +coverage.out +*.coverprofile + +# Temporary files +tmp/ +temp/ +*.tmp + +# Docker files +Dockerfile +Dockerfile.* +docker-compose*.yml +.dockerignore +.docker + +# CI/CD +.github +.gitlab-ci.yml +.circleci + +# Documentation +*.md +docs/ +README.md + +# Development files +Makefile +makefile +scripts/ +*.sh + +# Logs +*.log +logs/ + +# OS files +Thumbs.db diff --git a/gateway-service/Dockerfile b/gateway-service/Dockerfile index 829ca09..8caed65 100644 --- a/gateway-service/Dockerfile +++ b/gateway-service/Dockerfile @@ -1,8 +1,21 @@ -# Build stage -FROM golang:1.22-alpine AS builder +# Stage 1: Builder +FROM golang:1.26-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata openssh-client WORKDIR /app +# Configure Git for private repos +RUN git config --global url."git@github.com:".insteadOf "https://github.com/" + +# Copy SSH key for private repo access (build arg) +ARG SSH_PRIVATE_KEY +RUN mkdir -p /root/.ssh && \ + echo "${SSH_PRIVATE_KEY}" > /root/.ssh/id_rsa && \ + chmod 600 /root/.ssh/id_rsa && \ + ssh-keyscan -H github.com >> /root/.ssh/known_hosts + # Copy go mod files COPY go.mod go.sum ./ @@ -15,18 +28,23 @@ COPY . . # Build the application RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gateway-service . -# Final stage -FROM alpine:latest +# Stage 2: Runtime +FROM alpine:3.18 -RUN apk --no-cache add ca-certificates +RUN apk --no-cache add ca-certificates tzdata -WORKDIR /root/ +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser -# Copy binary from builder -COPY --from=builder /app/gateway-service . +WORKDIR /home/appuser + +COPY --from=builder --chown=appuser:appuser /app/gateway-service . + +USER appuser -# Expose port EXPOSE 8080 -# Run the application +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD nc -z localhost 8080 || exit 1 + CMD ["./gateway-service"] diff --git a/gateway-service/Dockerfile.prod b/gateway-service/Dockerfile.prod new file mode 100644 index 0000000..eb4a72d --- /dev/null +++ b/gateway-service/Dockerfile.prod @@ -0,0 +1,71 @@ +# Stage 1: Builder +FROM golang:1.26-alpine AS builder + +ARG VERSION=dev +ARG BUILD_TIME +ARG GIT_COMMIT + +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata make openssh-client + +WORKDIR /app + +# Configure Git for private repos +RUN git config --global url."git@github.com:".insteadOf "https://github.com/" + +# Copy SSH key for private repo access (build arg) +ARG SSH_PRIVATE_KEY +RUN mkdir -p /root/.ssh && \ + echo "${SSH_PRIVATE_KEY}" > /root/.ssh/id_rsa && \ + chmod 600 /root/.ssh/id_rsa && \ + ssh-keyscan -H github.com >> /root/.ssh/known_hosts + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies (cached layer) +RUN go mod download + +# Copy source code +COPY . . + +# Build with optimizations +ARG TARGETARCH + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build \ + -a \ + -installsuffix cgo \ + -ldflags="-w -s -X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.GitCommit=${GIT_COMMIT}" \ + -o gateway-service . + +# Stage 2: Runtime +FROM alpine:3.18 + +# Install minimal runtime dependencies +RUN apk --no-cache add ca-certificates tzdata curl + +# Create non-root user for security +RUN addgroup -g 1000 appuser && \ + adduser -D -u 1000 -G appuser appuser + +WORKDIR /home/appuser + +# Copy binary from builder +COPY --from=builder --chown=appuser:appuser /app/gateway-service . + +USER appuser + +# Expose HTTP port +EXPOSE 8080 + +# Metadata +LABEL org.opencontainers.image.title="HireMind Gateway Service" +LABEL org.opencontainers.image.description="HTTP API gateway for interview and auth services" +LABEL org.opencontainers.image.version="${VERSION}" + +# Health check against the HTTP /health endpoint +# HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ +# CMD curl -f http://localhost:8080/health || exit 1 + +# Run the application +CMD ["./gateway-service"] diff --git a/gateway-service/Makefile b/gateway-service/Makefile new file mode 100644 index 0000000..9fa3cfd --- /dev/null +++ b/gateway-service/Makefile @@ -0,0 +1,113 @@ +.PHONY: help build build-prod run stop clean logs compose-up compose-down compose-logs compose-rebuild docker-push version + +# Variables +SERVICE_NAME=gateway-service +DOCKER_REGISTRY=docker.io +DOCKER_IMAGE=$(DOCKER_REGISTRY)/hiremind/$(SERVICE_NAME) +VERSION?=latest +BUILD_TIME=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') +GIT_COMMIT=$(shell git rev-parse --short HEAD) +PORT=8080 + +# SSH key for pulling the private hiremind-proto-contracts dependency at build time. +# Override with: make build SSH_KEY=~/.ssh/some_other_key +SSH_KEY?=$(HOME)/.ssh/id_rsa +SSH_PRIVATE_KEY=$(shell cat $(SSH_KEY)) + +help: ## Display this help message + @echo "HireMind Gateway Service - Docker & Development Commands" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +# Docker Commands +build: ## Build Docker image + docker build \ + -t $(DOCKER_IMAGE):$(VERSION) \ + -t $(DOCKER_IMAGE):latest \ + --build-arg SSH_PRIVATE_KEY="$(SSH_PRIVATE_KEY)" \ + -f Dockerfile . + +build-prod: ## Build production Docker image + docker build \ + -t $(DOCKER_IMAGE):$(VERSION)-prod \ + --build-arg VERSION=$(VERSION) \ + --build-arg BUILD_TIME=$(BUILD_TIME) \ + --build-arg GIT_COMMIT=$(GIT_COMMIT) \ + --build-arg SSH_PRIVATE_KEY="$(SSH_PRIVATE_KEY)" \ + -f Dockerfile.prod . + +run: ## Run Docker container + docker run -it --rm \ + --env-file .env \ + -p $(PORT):$(PORT) \ + $(DOCKER_IMAGE):$(VERSION) + +stop: ## Stop the running container + docker stop hiremind-gateway-service || true + +docker-push: ## Push Docker image to registry + docker push $(DOCKER_IMAGE):$(VERSION) + docker push $(DOCKER_IMAGE):latest + +# Docker Compose Commands +compose-up: ## Start services with docker-compose + SSH_PRIVATE_KEY="$(SSH_PRIVATE_KEY)" docker-compose up -d + +compose-down: ## Stop services with docker-compose + docker-compose down + +compose-logs: ## View docker-compose logs + docker-compose logs -f + +compose-logs-service: ## View gateway-service logs only + docker-compose logs -f gateway-service + +compose-ps: ## Show running containers + docker-compose ps + +compose-rebuild: ## Rebuild container and start + SSH_PRIVATE_KEY="$(SSH_PRIVATE_KEY)" docker-compose up -d --build + +logs: ## Follow gateway-service container logs + docker logs -f hiremind-gateway-service + +# Development Commands +dev: ## Run service in development mode + go run main.go + +test: ## Run tests + go test -v ./... + +test-coverage: ## Run tests with coverage + go test -cover ./... -coverprofile=coverage.out + go tool cover -html=coverage.out + +lint: ## Run linter + golangci-lint run + +fmt: ## Format code + go fmt ./... + +clean: ## Clean build artifacts and dangling images + rm -f $(SERVICE_NAME) + docker image prune -f + +# Utility Commands +version: ## Display build information + @echo "Service: $(SERVICE_NAME)" + @echo "Version: $(VERSION)" + @echo "Docker Image: $(DOCKER_IMAGE)" + @echo "Build Time: $(BUILD_TIME)" + @echo "Git Commit: $(GIT_COMMIT)" + +health-check: ## Check service health + curl -f http://localhost:$(PORT)/health || echo "Service not healthy" + +image-size: ## Show Docker image size + docker images $(DOCKER_IMAGE) + +deps: ## Download Go dependencies + go mod download + go mod tidy + +.DEFAULT_GOAL := help diff --git a/gateway-service/docker-compose.yml b/gateway-service/docker-compose.yml new file mode 100644 index 0000000..e44f3cd --- /dev/null +++ b/gateway-service/docker-compose.yml @@ -0,0 +1,29 @@ +services: + gateway-service: + build: + context: . + dockerfile: Dockerfile + args: + SSH_PRIVATE_KEY: ${SSH_PRIVATE_KEY} + container_name: hiremind-gateway-service + volumes: + - ./.env:/.env:ro + env_file: + - .env + ports: + - "8080:8080" + networks: + - hiremind-network + restart: unless-stopped + healthcheck: + test: [ "CMD", "nc", "-z", "localhost", "8080" ] + interval: 30s + timeout: 3s + retries: 3 + +networks: + # Join the network created by the consumer-service compose project so that + # the gateway can resolve "consumer-service" via Docker DNS. + hiremind-network: + external: true + name: consumer-service_hiremind-network