a4dd60f6b8
Service accounts (SERVICE-ACCOUNTS.md) are a scoped, non-human credential: not a users row, so they never touch OIDC sync, login or the is_admin flag. Instance scope can create a team and mint a team-scoped account for it; team scope is owner-equivalent for that one team and nothing else. This is what unblocks terdut-operator's DESIGN.md §6 — no more impersonating a human admin, and a real rotation story instead of the unworkable delete-and-re-bootstrap /api/bootstrap can't actually do. - migration 014: service_accounts + service_account_keys - POST /api/service-accounts, POST/DELETE .../keys, GET ?name= self-lookup - AuthMiddleware resolves a tdsa_-prefixed key to a distinct principal; a team-scoped account gets a synthetic single membership so requireTeamMember/requireTeamOwner work on it unmodified - handleCreateTeam accepts an instance-scoped caller; the team it creates has no human owner, which is the expected shape for one an operator is about to hand a team-scoped credential to Operator mode (TERDUT_OPERATOR_MODE / values.operatorMode) declares an install gitops-managed: session and user-API-key writes to teams, escalation policies, dead man's switches and integrations get 403 reason=operator_managed, while a service account's writes still go through. Team membership/invites and the schedule are deliberately left out — never gitops-managed by design, and still human day-to-day work. /api/auth/config reports operator_mode so the web UI can grey these sections out from the start rather than only after a write fails. Also: GET /api/version (both terdut-tui and terdut-operator currently detect server capability by route-probing; this gives them a real answer), and a PUT for dead man's switches so a reconciler can update one in place instead of deleting and recreating it.
88 lines
2.4 KiB
Go
88 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.ryuvia.com/niklas/terdut-server/internal/api"
|
|
"git.ryuvia.com/niklas/terdut-server/internal/config"
|
|
"git.ryuvia.com/niklas/terdut-server/internal/db"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
if err := cfg.Validate(); err != nil {
|
|
log.Fatalf("config: %v", err)
|
|
}
|
|
|
|
database, err := db.Open(cfg.DSN)
|
|
if err != nil {
|
|
log.Fatalf("open db: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
if err := db.Migrate(database); err != nil {
|
|
log.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
notify := api.NotifyConfig{
|
|
BaseURL: cfg.NtfyURL,
|
|
Token: cfg.NtfyToken,
|
|
FallbackTopic: cfg.NtfyFallbackTopic,
|
|
PublicURL: cfg.PublicURL,
|
|
RepeatEvery: cfg.NotifyRepeat,
|
|
}
|
|
|
|
// Dead man's switches live per team now. The environment variables are the
|
|
// defaults a team starts from: every team without a configuration of its
|
|
// own gets one from them here, and an owner's later edit is never
|
|
// overwritten by a redeploy.
|
|
deadman := api.ParseDeadmanConfig(cfg.DeadmanMatchers, cfg.DeadmanTimeout, cfg.DeadmanSeverity)
|
|
if err := api.SeedDeadmanConfigs(context.Background(), database, deadman); err != nil {
|
|
log.Fatalf("seed dead man's switch defaults: %v", err)
|
|
}
|
|
|
|
// The behaviour knobs move into the database on first start, after which an
|
|
// administrator owns them and a redeploy leaves them alone.
|
|
if err := api.SeedSettings(context.Background(), database, cfg); err != nil {
|
|
log.Fatalf("seed settings: %v", err)
|
|
}
|
|
|
|
router := api.NewRouter(database, notify, cfg, version)
|
|
|
|
srv := &http.Server{
|
|
Addr: cfg.Addr,
|
|
Handler: router,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
go api.StartArchiver(ctx, database, cfg.ArchiveAfter, cfg.StaleAfter, notify)
|
|
go api.StartNotifier(ctx, database, notify)
|
|
|
|
go func() {
|
|
log.Printf("terdut-server %s listening on %s", version, cfg.Addr)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("listen: %v", err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
log.Println("shutting down")
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
log.Printf("shutdown: %v", err)
|
|
}
|
|
}
|