myxce/src/frontend/apiserver/cmd/main.go
Rongsong Shen 17e837b196
feat(build-time-versioning): apiserver -version flag and startup log
Declares GitCommit/BuildTime package vars (default "unknown", target of
task 4.1's -ldflags -X injection), a -version flag checked before
utils.PromptForTopDir()/InitLogger() (zero filesystem side effects), and
extends the existing "Initialize Logger done!" startup log line with
zap.String structured fields carrying both values, per design.md's
explicit mandate.

Live-verified end to end: -version output and the startup log's first
line are byte-identical, with a real commit id (matching
git rev-parse --short HEAD plus dirty suffix) and UTC timestamp -- the
first full round-trip proof in this spec (earlier Go tasks could only
verify via linked build-info metadata, since no consuming vars existed
yet).

Task 4.4/13 of build-time-versioning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 13:47:03 +00:00

137 lines
4.3 KiB
Go

package main
import (
_ "apiserver/docs"
"apiserver/backend"
"apiserver/backend/auth"
"apiserver/middleware"
"apiserver/router"
"apiserver/store"
"apiserver/utils"
logger "apiserver/utils"
"context"
"flag"
"fmt"
"io"
"os"
"apiserver/controllers"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// GitCommit and BuildTime are injected at link time via
// `-ldflags "-X main.GitCommit=... -X main.BuildTime=..."` (see
// src/tools/go_build and src/frontend/Makefile's build-server target). The
// "unknown" defaults matter: a bare `go build`/`go run` that bypasses that
// ldflags injection must still report "unknown" rather than an empty
// string, matching this feature's unknown-fallback requirement (see
// xce_version_field() in src/lib/utils.c for the equivalent C-side
// convention).
var (
GitCommit = "unknown"
BuildTime = "unknown"
)
func main() {
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
topDir := flag.String("d", "", "Top directory (default: current directory)")
versionFlag := flag.Bool("version", false, "Print version information and exit")
flag.Usage = func() {
fmt.Fprintln(os.Stderr, `
Usage: apiserver -d <top_directory>
Top directory (default: current directory)
`)
}
flag.Parse()
// Checked before utils.InitLogger() runs so -version has no logging
// side effects: it must only print and exit, matching controller's/
// scheduler's/worker's own --version contract (Requirement 2.1).
if *versionFlag {
fmt.Printf("xce-apiserver %s built %s\n", GitCommit, BuildTime)
return
}
utils.Global_TopDir = utils.PromptForTopDir(topDir)
if err := utils.InitLogger(); err != nil {
os.Exit(1)
}
logger.Info("********** Initialize Logger done!",
zap.String("gitCommit", GitCommit),
zap.String("buildTime", BuildTime))
if err := utils.InitConf(); err != nil {
logger.Error("Failed to initialize configuration files", zap.Error(err))
os.Exit(1)
}
logger.Info("********** Initialize configuration files done!")
// Initialize in-memory stores
store.InitStores()
logger.Info("********** Initialize in-memory stores done!")
// Connect to the XCE controller backend for live data.
// StartNotificationListener must be called before RegisterToBackend so the
// handler is ready before any push events can arrive.
if utils.BackendYamlConfig != nil && utils.BackendYamlConfig.Address != "" {
handler := &backend.DefaultHandler{}
if err := backend.StartNotificationListener(handler); err != nil {
logger.Error("Failed to start notification listener", zap.Error(err))
os.Exit(1)
}
bcfg := &backend.BackendConfig{
Address: utils.BackendYamlConfig.Address,
Port: utils.BackendYamlConfig.Port,
From: utils.BackendYamlConfig.From,
Key: utils.BackendYamlConfig.Key,
Peers: utils.BackendYamlConfig.Peers,
}
if err := backend.RegisterToBackend(bcfg); err != nil {
logger.Error("Failed to connect to backend", zap.Error(err))
os.Exit(1)
}
logger.Info("********** Backend connection registered!")
backend.StartHeartbeat(context.Background())
} else {
logger.Info("********** Backend not configured — running in stub mode")
}
// Load RBAC config
rbacPath := utils.Global_TopDir + "/config/rbac.yaml"
if err := controllers.LoadRBACConfig(rbacPath); err != nil {
logger.Error("Failed to load RBAC config", zap.Error(err))
os.Exit(1)
}
logger.Info("********** Load RBAC config done!")
// Initialize auth provider
authConfigPath := utils.Global_TopDir + "/config/auth.yaml"
if err := auth.InitAuthProvider(authConfigPath); err != nil {
logger.Error("Failed to initialize auth provider", zap.Error(err))
os.Exit(1)
}
logger.Info("********** Initialize auth provider done! (" + auth.GlobalProvider.Name() + ")")
// Pre-populate user-role store from auth provider so permissions work
// immediately after restart without requiring users to re-login.
if seeder, ok := auth.GlobalProvider.(auth.UserRoleSeeder); ok {
for _, username := range seeder.AllUsers() {
for _, role := range auth.GlobalProvider.GetUserRoles(username) {
store.AddRoleForUser(username, role)
}
}
logger.Info("********** Pre-populated user-role store from auth provider done!")
}
r := router.Router()
if err := middleware.StartServer(r); err != nil {
logger.Error("Server failed to start:", zap.Error(err))
os.Exit(1)
}
}