Initial scaffold: backend base + docker-compose dev infra + sveltekit frontend
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Package config carga la configuración del backend desde variables de entorno.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
)
|
||||
|
||||
// Config agrupa toda la configuración del servidor. Los campos de Zitadel/S3
|
||||
// ya están definidos aunque todavía no se usen: se conectan en las siguientes
|
||||
// fases sin tener que tocar esta struct de nuevo.
|
||||
type Config struct {
|
||||
Env string `env:"APP_ENV" envDefault:"development"`
|
||||
Port int `env:"PORT" envDefault:"8080"`
|
||||
|
||||
// Orígenes permitidos para CORS en desarrollo (el frontend en Vite corre en otro puerto).
|
||||
CORSAllowedOrigins []string `env:"CORS_ALLOWED_ORIGINS" envSeparator:"," envDefault:"http://localhost:5173"`
|
||||
|
||||
DatabaseURL string `env:"DATABASE_URL" envDefault:"postgres://dockermanager:dockermanager@localhost:5432/dockermanager?sslmode=disable"`
|
||||
|
||||
ZitadelIssuer string `env:"ZITADEL_ISSUER"`
|
||||
ZitadelClientID string `env:"ZITADEL_CLIENT_ID"`
|
||||
ZitadelClientSecret string `env:"ZITADEL_CLIENT_SECRET"`
|
||||
SessionSecret string `env:"SESSION_SECRET" envDefault:"dev-insecure-secret-change-me"`
|
||||
|
||||
S3Endpoint string `env:"S3_ENDPOINT" envDefault:"http://localhost:8333"`
|
||||
S3Bucket string `env:"S3_BUCKET" envDefault:"dockermanager"`
|
||||
S3AccessKey string `env:"S3_ACCESS_KEY"`
|
||||
S3SecretKey string `env:"S3_SECRET_KEY"`
|
||||
|
||||
// DockerHost vacío = usar el socket por defecto del sistema (DOCKER_HOST estándar del SDK).
|
||||
DockerHost string `env:"DOCKER_HOST"`
|
||||
}
|
||||
|
||||
// Load lee la configuración desde el entorno del proceso.
|
||||
func Load() (Config, error) {
|
||||
cfg := Config{}
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Package docker define la abstracción HostConnector: la interfaz que el
|
||||
// resto del backend usa para hablar con "un host Docker", sin importarle si
|
||||
// es el socket local (LocalConnector, fase 1) o un agente remoto
|
||||
// (AgentConnector, cuando se añada un segundo host más adelante).
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// Container es la representación simplificada que expone la API HTTP,
|
||||
// desacoplada del tipo interno del SDK de Docker.
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Names []string `json:"names"`
|
||||
Image string `json:"image"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Created int64 `json:"created"`
|
||||
}
|
||||
|
||||
// HostConnector es el contrato que debe cumplir cualquier forma de hablar con
|
||||
// un host Docker (local o remoto vía agente).
|
||||
type HostConnector interface {
|
||||
ListContainers(ctx context.Context) ([]Container, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// LocalConnector implementa HostConnector hablando directamente con el
|
||||
// socket Docker del host donde corre el backend.
|
||||
type LocalConnector struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
// NewLocalConnector crea un conector contra el Docker Engine local.
|
||||
// Si dockerHost está vacío, usa la configuración por defecto del SDK
|
||||
// (respeta la variable de entorno DOCKER_HOST si está definida).
|
||||
func NewLocalConnector(dockerHost string) (*LocalConnector, error) {
|
||||
opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
|
||||
if dockerHost != "" {
|
||||
opts = append(opts, client.WithHost(dockerHost))
|
||||
}
|
||||
|
||||
cli, err := client.NewClientWithOpts(opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating docker client: %w", err)
|
||||
}
|
||||
return &LocalConnector{cli: cli}, nil
|
||||
}
|
||||
|
||||
// ListContainers devuelve todos los contenedores del host, incluidos los
|
||||
// detenidos (equivalente a `docker ps -a`).
|
||||
func (l *LocalConnector) ListContainers(ctx context.Context) ([]Container, error) {
|
||||
list, err := l.cli.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing containers: %w", err)
|
||||
}
|
||||
|
||||
out := make([]Container, 0, len(list))
|
||||
for _, c := range list {
|
||||
out = append(out, Container{
|
||||
ID: c.ID,
|
||||
Names: c.Names,
|
||||
Image: c.Image,
|
||||
State: c.State,
|
||||
Status: c.Status,
|
||||
Created: c.Created,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Close libera los recursos del cliente Docker subyacente.
|
||||
func (l *LocalConnector) Close() error {
|
||||
return l.cli.Close()
|
||||
}
|
||||
|
||||
var _ io.Closer = (*LocalConnector)(nil)
|
||||
@@ -0,0 +1,29 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
dockerconn "dockermanager/backend/internal/docker"
|
||||
)
|
||||
|
||||
func handleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func handleListContainers(d dockerconn.HostConnector) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
containers, err := d.ListContainers(r.Context())
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, containers)
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Package httpserver contiene el router HTTP y los handlers de la API.
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
|
||||
dockerconn "dockermanager/backend/internal/docker"
|
||||
)
|
||||
|
||||
// Deps son las dependencias que necesitan los handlers.
|
||||
type Deps struct {
|
||||
Logger *slog.Logger
|
||||
Docker dockerconn.HostConnector
|
||||
Origins []string
|
||||
}
|
||||
|
||||
// NewRouter construye el router HTTP con todas las rutas de la Fase 1.
|
||||
func NewRouter(deps Deps) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.Timeout(15 * time.Second))
|
||||
r.Use(slogRequestLogger(deps.Logger))
|
||||
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: deps.Origins,
|
||||
AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
|
||||
AllowedHeaders: []string{"Accept", "Content-Type", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
|
||||
r.Get("/healthz", handleHealthz)
|
||||
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Get("/containers", handleListContainers(deps.Docker))
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// slogRequestLogger es un middleware mínimo que registra cada request con slog.
|
||||
func slogRequestLogger(logger *slog.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
next.ServeHTTP(ww, r)
|
||||
logger.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", ww.Status(),
|
||||
"duration_ms", time.Since(start).Milliseconds(),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user