build source

This commit is contained in:
build 2026-04-16 04:16:36 +00:00
commit ee1fec43ed
4171 changed files with 1351288 additions and 0 deletions

279
main.go Normal file
View file

@ -0,0 +1,279 @@
package main
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/api"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/forgejo"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/k8s"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/store"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Database
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
slog.Error("DATABASE_URL is required")
os.Exit(1)
}
pool, err := store.NewPool(ctx, dbURL)
if err != nil {
slog.Error("failed to connect to database", "error", err)
os.Exit(1)
}
defer pool.Close()
st := store.New(pool)
if err := st.Migrate(ctx); err != nil {
slog.Error("failed to run migrations", "error", err)
os.Exit(1)
}
slog.Info("database migrations complete")
// Bootstrap admin user if ADMIN_BOOTSTRAP_KEY is set and admin doesn't exist
bootstrapAdmin(ctx, st, logger)
// Kubernetes client
k8sCfg := k8s.ConfigFromEnv()
k8sClient, err := k8s.NewClient(k8sCfg)
if err != nil {
slog.Error("failed to create k8s client", "error", err)
os.Exit(1)
}
// Resource sampler (background goroutine)
sampler := k8s.NewResourceSampler(k8sClient, st, logger)
go sampler.Start(ctx)
// Forgejo integration (optional)
var forgejoManager api.ForgejoManager
forgejoAdminUser := os.Getenv("FORGEJO_ADMIN_USER")
forgejoAdminPassword := os.Getenv("FORGEJO_ADMIN_PASSWORD")
if forgejoAdminUser != "" && forgejoAdminPassword != "" {
forgejoManager = forgejo.NewClient(k8sCfg.ForgejoURL, forgejoAdminUser, forgejoAdminPassword)
slog.Info("forgejo integration enabled", "url", k8sCfg.ForgejoURL)
} else {
slog.Info("forgejo integration disabled (FORGEJO_ADMIN_USER/FORGEJO_ADMIN_PASSWORD not set)")
}
// web-tui: JWT signer (optional — only activates /api/v1/auth/exchange
// and /.well-known/jwks.json when WEBTUI_JWT_SEED is set).
jwtSigner := loadJWTSigner(logger)
// web-tui: session-host manager (optional — activates
// /api/v1/pods/session-host endpoints when WEBTUI_ENABLED=1).
var sessionHost api.SessionHostManager
if os.Getenv("WEBTUI_ENABLED") == "1" {
spec := k8s.SessionHostSpec{
Image: envOr("WEBTUI_GOLDEN_IMAGE", "10.22.0.56:30500/dev-golden:v2"),
PortWatchImage: envOr("WEBTUI_PORTWATCH_IMAGE", "10.22.0.56:30500/web-tui-port-watch:dev"),
ApexDomain: envOr("WEBTUI_APEX_DOMAIN", "spinoff.dev"),
ClusterIssuer: envOr("WEBTUI_CLUSTER_ISSUER", "letsencrypt-cloudflare-dns01"),
GatewayService: envOr("WEBTUI_GATEWAY_SERVICE", "web-tui-gateway"),
GatewayServiceNamespace: envOr("WEBTUI_GATEWAY_NAMESPACE", "web-tui"),
}
sessionHost = &sessionHostAdapter{client: k8sClient, spec: spec}
logger.Info("web-tui session-host manager enabled", "apex", spec.ApexDomain)
}
// Runner cleanup goroutine
runnerCleaner := k8s.NewRunnerCleaner(k8sClient, st, logger)
go runnerCleaner.Start(ctx)
// HTTP server
srv := &api.Server{
Store: st,
K8s: k8sClient,
Cluster: k8sClient,
Users: st,
Usage: st,
Forgejo: forgejoManager,
Runners: st,
RunnerPods: k8sClient,
Logger: logger,
GenerateKey: store.GenerateAPIKey,
JWTSigner: jwtSigner,
SessionHost: sessionHost,
WebhookSecret: os.Getenv("FORGEJO_WEBHOOK_SECRET"),
ForgejoRunnerToken: os.Getenv("FORGEJO_RUNNER_TOKEN"),
}
router := api.NewRouter(srv)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
httpSrv := &http.Server{
Addr: ":" + port,
Handler: router,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
}
go func() {
<-ctx.Done()
slog.Info("shutting down server")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := httpSrv.Shutdown(shutdownCtx); err != nil {
slog.Error("server shutdown error", "error", err)
}
}()
slog.Info("starting dev-pod-api", "port", port)
if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server failed", "error", err)
os.Exit(1)
}
slog.Info("server stopped")
}
// envOr returns the named env var if set, otherwise fallback.
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
// loadJWTSigner returns a signer backed by the base64-encoded seed in
// WEBTUI_JWT_SEED (32-byte Ed25519 seed). Missing env → nil signer →
// /api/v1/auth/exchange returns 503, which is fine when web-tui isn't
// in use.
func loadJWTSigner(logger *slog.Logger) api.JWTSigner {
raw := os.Getenv("WEBTUI_JWT_SEED")
var seed []byte
if raw == "" {
// Dev convenience: synthesise a fresh seed at startup. All JWTs
// issued with this key become invalid on restart. For prod set
// WEBTUI_JWT_SEED from a Secret.
seed = make([]byte, ed25519.SeedSize)
if _, err := rand.Read(seed); err != nil {
logger.Error("failed to generate JWT seed", "error", err)
return nil
}
logger.Warn("WEBTUI_JWT_SEED unset — generated an ephemeral key; tokens will be invalidated on restart")
} else {
var err error
seed, err = base64.StdEncoding.DecodeString(raw)
if err != nil {
logger.Error("WEBTUI_JWT_SEED is not valid base64", "error", err)
return nil
}
}
signer, err := api.NewEd25519SignerFromSeed(envOr("WEBTUI_JWT_KID", "webtui-1"), seed)
if err != nil {
logger.Error("failed to construct JWT signer", "error", err)
return nil
}
return signer
}
// sessionHostAdapter bridges k8s.Client into the api.SessionHostManager
// interface, translating k8s.SessionHostStatus → api.SessionHostStatus.
type sessionHostAdapter struct {
client *k8s.Client
spec k8s.SessionHostSpec
}
func (a *sessionHostAdapter) EnsureForUser(ctx context.Context, user string) (api.SessionHostStatus, error) {
s, err := a.client.EnsureSessionHost(ctx, user, a.spec)
return toAPIStatus(s), err
}
func (a *sessionHostAdapter) StatusForUser(ctx context.Context, user string) (api.SessionHostStatus, error) {
s, err := a.client.SessionHostStatusForUser(ctx, user)
return toAPIStatus(s), err
}
func toAPIStatus(s k8s.SessionHostStatus) api.SessionHostStatus {
return api.SessionHostStatus{
Ready: s.Ready,
PodName: s.PodName,
Namespace: s.Namespace,
CertReady: s.CertReady,
CertPendingReason: s.CertPendingReason,
AtchSessionCount: s.AtchSessionCount,
CreatedAt: s.CreatedAt,
}
}
// bootstrapAdmin creates an admin user with an API key on first run.
// Set ADMIN_BOOTSTRAP_KEY to a pre-chosen key, or leave empty to auto-generate one.
func bootstrapAdmin(ctx context.Context, st *store.Store, logger *slog.Logger) {
// Check if admin user already exists
_, err := st.GetUser(ctx, "admin")
if err == nil {
logger.Info("admin user already exists, skipping bootstrap")
return
}
if !errors.Is(err, store.ErrNotFound) {
logger.Error("failed to check for admin user", "error", err)
return
}
// Create admin user with elevated quotas
adminQuota := model.Quota{
MaxConcurrentPods: 10,
MaxCPUPerPod: 16,
MaxRAMGBPerPod: 32,
MonthlyPodHours: 10000,
MonthlyAIRequests: 100000,
}
if _, err := st.CreateUser(ctx, "admin", adminQuota); err != nil {
logger.Error("failed to create admin user", "error", err)
return
}
// Use pre-set key or generate one
bootstrapKey := os.Getenv("ADMIN_BOOTSTRAP_KEY")
var plainKey, keyHash string
if bootstrapKey != "" {
plainKey = bootstrapKey
keyHash = store.HashKey(plainKey)
} else {
var genErr error
plainKey, keyHash, genErr = store.GenerateAPIKey()
if genErr != nil {
logger.Error("failed to generate admin API key", "error", genErr)
if delErr := st.DeleteUser(ctx, "admin"); delErr != nil {
logger.Error("rollback: failed to delete orphaned admin user", "error", delErr)
}
return
}
}
if err := st.CreateAPIKey(ctx, "admin", model.RoleAdmin, keyHash); err != nil {
logger.Error("failed to create admin API key", "error", err)
if delErr := st.DeleteUser(ctx, "admin"); delErr != nil {
logger.Error("rollback: failed to delete orphaned admin user", "error", delErr)
}
return
}
logger.Info("admin user bootstrapped successfully")
// Print the key to stdout so it can be captured from logs
fmt.Fprintf(os.Stderr, "\n=== ADMIN API KEY (save this, it won't be shown again) ===\n%s\n===\n\n", plainKey)
}