Compare commits
1 Commits
1b86d617cd
...
agent/hex/
| Author | SHA1 | Date | |
|---|---|---|---|
| a5a9f42d06 |
@@ -1,25 +1,37 @@
|
|||||||
# Build stage
|
# ── Stage 1: Build ──────────────────────────────────────────
|
||||||
FROM golang:1.24-alpine AS builder
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy csproj first for layer caching — restores before copying source
|
||||||
|
COPY Extrudex.csproj .
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
# Copy the rest of the source
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish Extrudex.csproj \
|
||||||
|
-c Release \
|
||||||
|
-o /app/publish \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
# ── Stage 2: Runtime ────────────────────────────────────────
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy go mod files first for caching
|
# Install curl for health check (not included in aspnet base image)
|
||||||
COPY go.mod go.sum ./
|
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Copy source and build
|
# Non-root user for security
|
||||||
COPY . .
|
RUN adduser --disabled-password --gecos "" appuser
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server
|
USER appuser
|
||||||
|
|
||||||
# Final stage
|
# Copy published output from build stage
|
||||||
FROM alpine:latest
|
COPY --from=build /app/publish .
|
||||||
RUN apk --no-cache add ca-certificates
|
|
||||||
|
|
||||||
WORKDIR /root/
|
|
||||||
|
|
||||||
# Copy binary from builder
|
|
||||||
COPY --from=builder /app/server .
|
|
||||||
|
|
||||||
|
# ASP.NET Core listens on 8080 by default in .NET 8+
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["./server"]
|
# Health check against /health endpoint
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl --fail http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "Extrudex.dll"]
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/CubeCraft-Creations/Extrudex/backend/internal/config"
|
|
||||||
"github.com/CubeCraft-Creations/Extrudex/backend/internal/db"
|
|
||||||
"github.com/CubeCraft-Creations/Extrudex/backend/internal/router"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Setup structured logging
|
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
|
||||||
Level: slog.LevelInfo,
|
|
||||||
})))
|
|
||||||
|
|
||||||
// Load configuration
|
|
||||||
cfg, err := config.Load()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to load config", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("config loaded", "port", cfg.Port, "cors_origin", cfg.CorsOrigin)
|
|
||||||
|
|
||||||
// Connect to database
|
|
||||||
dbPool, err := db.NewPool(cfg.DatabaseURL)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to connect to database", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer db.ClosePool(dbPool)
|
|
||||||
|
|
||||||
slog.Info("database connected")
|
|
||||||
|
|
||||||
// Create router
|
|
||||||
r := router.New(cfg, dbPool)
|
|
||||||
|
|
||||||
// Create HTTP server
|
|
||||||
server := &http.Server{
|
|
||||||
Addr: ":" + cfg.Port,
|
|
||||||
Handler: r,
|
|
||||||
ReadTimeout: 15 * time.Second,
|
|
||||||
WriteTimeout: 15 * time.Second,
|
|
||||||
IdleTimeout: 60 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start server in goroutine
|
|
||||||
go func() {
|
|
||||||
slog.Info("server starting", "addr", server.Addr)
|
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
slog.Error("server error", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for shutdown signal
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-quit
|
|
||||||
|
|
||||||
slog.Info("server shutting down")
|
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := server.Shutdown(ctx); err != nil {
|
|
||||||
slog.Error("server shutdown error", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db.ClosePool(dbPool)
|
|
||||||
slog.Info("server stopped")
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,5 @@ module github.com/CubeCraft-Creations/Extrudex/backend
|
|||||||
go 1.24
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.2.0
|
|
||||||
github.com/jackc/pgx/v5 v5.7.4
|
github.com/jackc/pgx/v5 v5.7.4
|
||||||
github.com/kelseyhightower/envconfig v1.4.0
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/kelseyhightower/envconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config holds all application configuration loaded from environment variables.
|
|
||||||
type Config struct {
|
|
||||||
DatabaseURL string `envconfig:"database_url" required:"true"`
|
|
||||||
Port string `envconfig:"port" default:"8080"`
|
|
||||||
CorsOrigin string `envconfig:"cors_origin" default:"*"`
|
|
||||||
LogLevel string `envconfig:"log_level" default:"info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load reads configuration from environment variables and returns a populated Config.
|
|
||||||
func Load() (*Config, error) {
|
|
||||||
var cfg Config
|
|
||||||
if err := envconfig.Process("", &cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load config: %w", err)
|
|
||||||
}
|
|
||||||
return &cfg, nil
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewPool creates a new pgx connection pool and verifies connectivity with a ping.
|
|
||||||
func NewPool(databaseURL string) (*pgxpool.Pool, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
pool, err := pgxpool.New(ctx, databaseURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create db pool: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pool.Ping(ctx); err != nil {
|
|
||||||
pool.Close()
|
|
||||||
return nil, fmt.Errorf("failed to ping db: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClosePool gracefully closes the connection pool.
|
|
||||||
func ClosePool(pool *pgxpool.Pool) {
|
|
||||||
if pool != nil {
|
|
||||||
pool.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HealthHandler provides a health check endpoint that verifies database connectivity.
|
|
||||||
type HealthHandler struct {
|
|
||||||
dbPool *pgxpool.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHealthHandler creates a new HealthHandler with the given database pool.
|
|
||||||
func NewHealthHandler(dbPool *pgxpool.Pool) *HealthHandler {
|
|
||||||
return &HealthHandler{dbPool: dbPool}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeHTTP handles GET /health requests.
|
|
||||||
func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
dbConnected := false
|
|
||||||
if h.dbPool != nil {
|
|
||||||
if err := h.dbPool.Ping(ctx); err == nil {
|
|
||||||
dbConnected = true
|
|
||||||
} else {
|
|
||||||
slog.Warn("health check db ping failed", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := map[string]any{
|
|
||||||
"status": "ok",
|
|
||||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
||||||
"db_connected": dbConnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
if !dbConnected {
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
||||||
slog.Error("failed to encode health response", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Repositories
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package router
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/CubeCraft-Creations/Extrudex/backend/internal/config"
|
|
||||||
"github.com/CubeCraft-Creations/Extrudex/backend/internal/handlers"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
)
|
|
||||||
|
|
||||||
// New creates and configures a Chi router with all middleware and handlers mounted.
|
|
||||||
func New(cfg *config.Config, dbPool *pgxpool.Pool) chi.Router {
|
|
||||||
r := chi.NewRouter()
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
r.Use(middleware.RequestID)
|
|
||||||
r.Use(middleware.RealIP)
|
|
||||||
r.Use(middleware.Logger)
|
|
||||||
r.Use(middleware.Recoverer)
|
|
||||||
r.Use(middleware.Timeout(60 * time.Second))
|
|
||||||
|
|
||||||
// CORS
|
|
||||||
r.Use(func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", cfg.CorsOrigin)
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
||||||
if r.Method == http.MethodOptions {
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Health check
|
|
||||||
healthHandler := handlers.NewHealthHandler(dbPool)
|
|
||||||
r.Get("/health", healthHandler.ServeHTTP)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Services
|
|
||||||
Reference in New Issue
Block a user