CUB-124: scaffold Control Center Go backend
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m6s

This commit is contained in:
2026-05-07 14:16:05 -04:00
parent cce3e061a7
commit c906cd46ad
7 changed files with 206 additions and 15 deletions

35
go-backend/.dockerignore Normal file
View File

@@ -0,0 +1,35 @@
# Ignore local build artifacts and version-control files
*.exe
*.dll
*.so
*.dylib
*.test
*.out
bin/
dist/
# Version control
.git
.gitignore
# IDE / editor
.idea
.vscode
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Dependency cache (already fetched in Dockerfile)
vendor/
# Documentation
README.md
*.md
# CI / CD
.github/
.gitea/

35
go-backend/Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# Build stage
FROM golang:1.23-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git ca-certificates
# Copy dependency files first for better layer caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the binary
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /bin/server ./cmd/server
# ── Final stage ─────────────────────────────────────────────────────────
FROM alpine:latest
WORKDIR /app
# Install ca-certificates for HTTPS outbound calls
RUN apk --no-cache add ca-certificates
# Copy binary from builder
COPY --from=builder /bin/server /app/server
# Expose the default port (overridden by PORT env var)
EXPOSE 8080
# Run as non-root
RUN adduser -D -s /bin/sh appuser
USER appuser
ENTRYPOINT ["/app/server"]

View File

@@ -12,6 +12,7 @@ import (
"time"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/config"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/db"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/handler"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/router"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/store"
@@ -27,6 +28,16 @@ func main() {
}))
slog.SetDefault(logger)
// ── Database (optional until CUB-120 schema is ready) ──────────────────
var pool *db.Pool
if cfg.DatabaseURL != "" {
var err error
pool, err = db.New(cfg.DatabaseURL)
if err != nil {
slog.Warn("database connection failed; running without DB", "error", err)
}
}
// ── Stores (in-memory for now; PostgreSQL after CUB-120) ────────────────
agentStore := store.NewAgentStore()
sessionStore := store.NewSessionStore()
@@ -37,7 +48,11 @@ func main() {
h := handler.NewHandler(agentStore, sessionStore, taskStore, projectStore)
// ── Router ─────────────────────────────────────────────────────────────
r := router.New(h)
r := router.New(&router.Dependencies{
Handler: h,
DB: pool,
CORSOrigin: cfg.CORSOrigin,
})
// ── Server ─────────────────────────────────────────────────────────────
srv := &http.Server{
@@ -71,6 +86,10 @@ func main() {
os.Exit(1)
}
if pool != nil {
pool.Close()
}
slog.Info("server exited cleanly")
}

View File

@@ -7,15 +7,20 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-playground/validator/v10 v10.24.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.2
)
require (
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
)

View File

@@ -1,3 +1,4 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
@@ -16,19 +17,34 @@ github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,62 @@
// Package db provides PostgreSQL connection management using pgx.
package db
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// Pool wraps a pgx connection pool with lifecycle helpers.
type Pool struct {
*pgxpool.Pool
}
// New creates a connection pool from a PostgreSQL DSN.
func New(dsn string) (*Pool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("parse pgx config: %w", err)
}
// Sensible defaults
cfg.MaxConns = 20
cfg.MinConns = 2
cfg.MaxConnLifetime = 30 * time.Minute
cfg.MaxConnIdleTime = 10 * time.Minute
cfg.HealthCheckPeriod = 5 * time.Second
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("create pgx pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping database: %w", err)
}
slog.Info("database connected", "pool", cfg.ConnConfig.Database)
return &Pool{Pool: pool}, nil
}
// Close shuts down the pool gracefully.
func (p *Pool) Close() {
if p.Pool != nil {
p.Pool.Close()
slog.Info("database pool closed")
}
}
// Health returns nil if the database is reachable.
func (p *Pool) Health(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
return p.Ping(ctx)
}

View File

@@ -6,14 +6,22 @@ import (
"net/http"
"time"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/db"
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/handler"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
// Dependencies carries the handler and database pool into the router.
type Dependencies struct {
Handler *handler.Handler
DB *db.Pool
CORSOrigin string
}
// New creates a fully-configured chi router with all API routes mounted.
func New(h *handler.Handler) *chi.Mux {
func New(deps *Dependencies) *chi.Mux {
r := chi.NewRouter()
// ── Global middleware ──────────────────────────────────────────────────
@@ -23,9 +31,13 @@ func New(h *handler.Handler) *chi.Mux {
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
// ── CORS — permissive for development ──────────────────────────────────
// ── CORS ───────────────────────────────────────────────────────────────
corsOrigin := deps.CORSOrigin
if corsOrigin == "" {
corsOrigin = "*"
}
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedOrigins: []string{corsOrigin},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
ExposedHeaders: []string{"Link", "X-Total-Count"},
@@ -33,32 +45,39 @@ func New(h *handler.Handler) *chi.Mux {
MaxAge: 300,
}))
// ── Health check ───────────────────────────────────────────────────────
// ── Health check (with DB connectivity probe) ──────────────────────────
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
status := "ok"
if deps.DB != nil {
if err := deps.DB.Health(r.Context()); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
status = "db_unhealthy"
}
}
w.Write([]byte(`{"status":"` + status + `"}`))
})
// ── API v1 routes ──────────────────────────────────────────────────────
r.Route("/api", func(api chi.Router) {
// Agents CRUD
api.Route("/agents", func(agents chi.Router) {
agents.Get("/", h.ListAgents) // GET /api/agents
agents.Post("/", h.CreateAgent) // POST /api/agents
agents.Get("/{id}", h.GetAgent) // GET /api/agents/{id}
agents.Put("/{id}", h.UpdateAgent) // PUT /api/agents/{id}
agents.Delete("/{id}", h.DeleteAgent) // DELETE /api/agents/{id}
agents.Get("/{id}/history", h.AgentHistory) // GET /api/agents/{id}/history
agents.Get("/", deps.Handler.ListAgents) // GET /api/agents
agents.Post("/", deps.Handler.CreateAgent) // POST /api/agents
agents.Get("/{id}", deps.Handler.GetAgent) // GET /api/agents/{id}
agents.Put("/{id}", deps.Handler.UpdateAgent) // PUT /api/agents/{id}
agents.Delete("/{id}", deps.Handler.DeleteAgent) // DELETE /api/agents/{id}
agents.Get("/{id}/history", deps.Handler.AgentHistory) // GET /api/agents/{id}/history
})
// Sessions
api.Get("/sessions", h.ListSessions)
api.Get("/sessions", deps.Handler.ListSessions)
// Tasks
api.Get("/tasks", h.ListTasks)
api.Get("/tasks", deps.Handler.ListTasks)
// Projects
api.Get("/projects", h.ListProjects)
api.Get("/projects", deps.Handler.ListProjects)
})
return r