feat: add first page with auth and containers list and agents

This commit is contained in:
2026-05-18 08:24:02 +02:00
parent 446087ae01
commit 3b4a841bf5
56 changed files with 16267 additions and 0 deletions

139
server/cmd/server/main.go Normal file
View File

@ -0,0 +1,139 @@
package main
import (
"context"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"github.com/containarr/server/internal/api"
"github.com/containarr/server/internal/broker"
grpcgateway "github.com/containarr/server/internal/grpc"
agentv1 "github.com/containarr/server/internal/proto/agentv1"
"github.com/containarr/server/internal/store"
"google.golang.org/grpc"
)
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
dbPath := getenv("DB_PATH", "/data/containarr.db")
httpAddr := getenv("HTTP_ADDR", ":8080")
grpcAddr := getenv("GRPC_ADDR", ":9090")
db, err := store.New(dbPath)
must(err, "open store")
defer db.Close()
bootstrapAdmin(db)
bootstrapTokens(db)
reg := grpcgateway.NewRegistry()
brk := broker.New()
// gRPC server.
gw := grpcgateway.NewGateway(db, reg, brk)
grpcServer := grpc.NewServer()
agentv1.RegisterAgentGatewayServer(grpcServer, gw)
lis, err := net.Listen("tcp", grpcAddr)
must(err, "listen grpc")
go func() {
slog.Info("gRPC listening", "addr", grpcAddr)
if err := grpcServer.Serve(lis); err != nil {
slog.Error("gRPC serve", "err", err)
}
}()
// HTTP server.
h := api.NewHandler(db, reg, brk)
httpServer := &http.Server{
Addr: httpAddr,
Handler: api.NewRouter(h),
ReadTimeout: 10 * time.Second,
WriteTimeout: 0, // disabled for WebSocket handlers
}
go func() {
slog.Info("HTTP listening", "addr", httpAddr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("HTTP serve", "err", err)
}
}()
// Graceful shutdown.
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("shutting down")
grpcServer.GracefulStop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = httpServer.Shutdown(ctx)
}
func getenv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
// bootstrapAdmin creates the admin user from env vars if it doesn't exist yet.
func bootstrapAdmin(db *store.Store) {
username := getenv("ADMIN_USER", "admin")
password := getenv("ADMIN_PASSWORD", "admin")
exists, err := db.UserExists(username)
if err != nil || exists {
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
slog.Error("bcrypt admin", "err", err)
return
}
if err := db.UpsertUser(username, string(hash)); err != nil {
slog.Error("seed admin user", "err", err)
return
}
slog.Info("admin user created", "username", username)
}
// bootstrapTokens seeds agent tokens from BOOTSTRAP_TOKENS env var.
// Format: "hostname:token,hostname2:token2"
func bootstrapTokens(db *store.Store) {
raw := os.Getenv("BOOTSTRAP_TOKENS")
if raw == "" {
return
}
for _, pair := range strings.Split(raw, ",") {
parts := strings.SplitN(strings.TrimSpace(pair), ":", 2)
if len(parts) != 2 {
continue
}
hostname, token := parts[0], parts[1]
if err := db.CreateAgentToken(uuid.NewString(), token, hostname); err != nil {
slog.Warn("bootstrap token already exists", "hostname", hostname)
} else {
slog.Info("bootstrapped agent token", "hostname", hostname)
}
}
}
func must(err error, msg string) {
if err != nil {
slog.Error(msg, "err", err)
os.Exit(1)
}
}