mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-11-26 03:23:07 +01:00
Refactor main package
Fix issues. Break down main routine. Add documentation. Remove globals.
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
// Package main provides the entry point for the ClusterCockpit backend server.
|
||||||
|
// This file defines all command-line flags and their default values.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "flag"
|
import "flag"
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
// Package main provides the entry point for the ClusterCockpit backend server.
|
||||||
|
// This file contains bootstrap logic for initializing the environment,
|
||||||
|
// creating default configuration files, and setting up the database.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
// Package main provides the entry point for the ClusterCockpit backend server.
|
||||||
|
// It orchestrates initialization of all subsystems including configuration,
|
||||||
|
// database, authentication, and the HTTP server.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,6 +17,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/archiver"
|
"github.com/ClusterCockpit/cc-backend/internal/archiver"
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/auth"
|
"github.com/ClusterCockpit/cc-backend/internal/auth"
|
||||||
@@ -46,90 +51,108 @@ const logoString = `
|
|||||||
|_|
|
|_|
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Environment variable names
|
||||||
|
const (
|
||||||
|
envGOGC = "GOGC"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default configurations
|
||||||
|
const (
|
||||||
|
defaultArchiveConfig = `{"kind":"file","path":"./var/job-archive"}`
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
date string
|
date string
|
||||||
commit string
|
commit string
|
||||||
version string
|
version string
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func printVersion() {
|
||||||
cliInit()
|
|
||||||
|
|
||||||
if flagVersion {
|
|
||||||
fmt.Print(logoString)
|
fmt.Print(logoString)
|
||||||
fmt.Printf("Version:\t%s\n", version)
|
fmt.Printf("Version:\t%s\n", version)
|
||||||
fmt.Printf("Git hash:\t%s\n", commit)
|
fmt.Printf("Git hash:\t%s\n", commit)
|
||||||
fmt.Printf("Build time:\t%s\n", date)
|
fmt.Printf("Build time:\t%s\n", date)
|
||||||
fmt.Printf("SQL db version:\t%d\n", repository.Version)
|
fmt.Printf("SQL db version:\t%d\n", repository.Version)
|
||||||
fmt.Printf("Job archive version:\t%d\n", archive.Version)
|
fmt.Printf("Job archive version:\t%d\n", archive.Version)
|
||||||
os.Exit(0)
|
}
|
||||||
|
|
||||||
|
func initGops() error {
|
||||||
|
if !flagGops {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cclog.Init(flagLogLevel, flagLogDateTime)
|
|
||||||
|
|
||||||
// If init flag set, run tasks here before any file dependencies cause errors
|
|
||||||
if flagInit {
|
|
||||||
initEnv()
|
|
||||||
cclog.Exit("Successfully setup environment!\n" +
|
|
||||||
"Please review config.json and .env and adjust it to your needs.\n" +
|
|
||||||
"Add your job-archive at ./var/job-archive.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://github.com/google/gops (Runtime overhead is almost zero)
|
|
||||||
if flagGops {
|
|
||||||
if err := agent.Listen(agent.Options{}); err != nil {
|
if err := agent.Listen(agent.Options{}); err != nil {
|
||||||
cclog.Abortf("Could not start gops agent with 'gops/agent.Listen(agent.Options{})'. Application startup failed, exited.\nError: %s\n", err.Error())
|
return fmt.Errorf("starting gops agent: %w", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
err := godotenv.Load()
|
func loadEnvironment() error {
|
||||||
if err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
cclog.Abortf("Could not parse existing .env file at location './.env'. Application startup failed, exited.\nError: %s\n", err.Error())
|
return fmt.Errorf("loading .env file: %w", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize sub-modules and handle command line flags.
|
func initConfiguration() error {
|
||||||
// The order here is important!
|
|
||||||
ccconf.Init(flagConfigFile)
|
ccconf.Init(flagConfigFile)
|
||||||
|
|
||||||
// Load and check main configuration
|
cfg := ccconf.GetPackageConfig("main")
|
||||||
if cfg := ccconf.GetPackageConfig("main"); cfg != nil {
|
if cfg == nil {
|
||||||
if clustercfg := ccconf.GetPackageConfig("clusters"); clustercfg != nil {
|
return fmt.Errorf("main configuration must be present")
|
||||||
config.Init(cfg, clustercfg)
|
|
||||||
} else {
|
|
||||||
cclog.Abort("Cluster configuration must be present")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cclog.Abort("Main configuration must be present")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clustercfg := ccconf.GetPackageConfig("clusters")
|
||||||
|
if clustercfg == nil {
|
||||||
|
return fmt.Errorf("cluster configuration must be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Init(cfg, clustercfg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDatabase() error {
|
||||||
|
repository.Connect(config.Keys.DBDriver, config.Keys.DB)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDatabaseCommands() error {
|
||||||
if flagMigrateDB {
|
if flagMigrateDB {
|
||||||
err := repository.MigrateDB(config.Keys.DBDriver, config.Keys.DB)
|
err := repository.MigrateDB(config.Keys.DBDriver, config.Keys.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Abortf("MigrateDB Failed: Could not migrate '%s' database at location '%s' to version %d.\nError: %s\n", config.Keys.DBDriver, config.Keys.DB, repository.Version, err.Error())
|
return fmt.Errorf("migrating database to version %d: %w", repository.Version, err)
|
||||||
}
|
}
|
||||||
cclog.Exitf("MigrateDB Success: Migrated '%s' database at location '%s' to version %d.\n", config.Keys.DBDriver, config.Keys.DB, repository.Version)
|
cclog.Exitf("MigrateDB Success: Migrated '%s' database at location '%s' to version %d.\n",
|
||||||
|
config.Keys.DBDriver, config.Keys.DB, repository.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagRevertDB {
|
if flagRevertDB {
|
||||||
err := repository.RevertDB(config.Keys.DBDriver, config.Keys.DB)
|
err := repository.RevertDB(config.Keys.DBDriver, config.Keys.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Abortf("RevertDB Failed: Could not revert '%s' database at location '%s' to version %d.\nError: %s\n", config.Keys.DBDriver, config.Keys.DB, (repository.Version - 1), err.Error())
|
return fmt.Errorf("reverting database to version %d: %w", repository.Version-1, err)
|
||||||
}
|
}
|
||||||
cclog.Exitf("RevertDB Success: Reverted '%s' database at location '%s' to version %d.\n", config.Keys.DBDriver, config.Keys.DB, (repository.Version - 1))
|
cclog.Exitf("RevertDB Success: Reverted '%s' database at location '%s' to version %d.\n",
|
||||||
|
config.Keys.DBDriver, config.Keys.DB, repository.Version-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagForceDB {
|
if flagForceDB {
|
||||||
err := repository.ForceDB(config.Keys.DBDriver, config.Keys.DB)
|
err := repository.ForceDB(config.Keys.DBDriver, config.Keys.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Abortf("ForceDB Failed: Could not force '%s' database at location '%s' to version %d.\nError: %s\n", config.Keys.DBDriver, config.Keys.DB, repository.Version, err.Error())
|
return fmt.Errorf("forcing database to version %d: %w", repository.Version, err)
|
||||||
}
|
}
|
||||||
cclog.Exitf("ForceDB Success: Forced '%s' database at location '%s' to version %d.\n", config.Keys.DBDriver, config.Keys.DB, repository.Version)
|
cclog.Exitf("ForceDB Success: Forced '%s' database at location '%s' to version %d.\n",
|
||||||
|
config.Keys.DBDriver, config.Keys.DB, repository.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
repository.Connect(config.Keys.DBDriver, config.Keys.DB)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUserCommands() error {
|
||||||
|
if config.Keys.DisableAuthentication && (flagNewUser != "" || flagDelUser != "") {
|
||||||
|
return fmt.Errorf("--add-user and --del-user can only be used if authentication is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
if !config.Keys.DisableAuthentication {
|
if !config.Keys.DisableAuthentication {
|
||||||
|
|
||||||
if cfg := ccconf.GetPackageConfig("auth"); cfg != nil {
|
if cfg := ccconf.GetPackageConfig("auth"); cfg != nil {
|
||||||
auth.Init(&cfg)
|
auth.Init(&cfg)
|
||||||
} else {
|
} else {
|
||||||
@@ -137,51 +160,96 @@ func main() {
|
|||||||
auth.Init(nil)
|
auth.Init(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagNewUser != "" {
|
// Check for default security keys
|
||||||
parts := strings.SplitN(flagNewUser, ":", 3)
|
checkDefaultSecurityKeys()
|
||||||
if len(parts) != 3 || len(parts[0]) == 0 {
|
|
||||||
cclog.Abortf("Add User: Could not parse supplied argument format: No changes.\n"+
|
|
||||||
"Want: <username>:[admin,support,manager,api,user]:<password>\n"+
|
|
||||||
"Have: %s\n", flagNewUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
ur := repository.GetUserRepository()
|
if flagNewUser != "" {
|
||||||
if err := ur.AddUser(&schema.User{
|
if err := addUser(flagNewUser); err != nil {
|
||||||
Username: parts[0], Projects: make([]string, 0), Password: parts[2], Roles: strings.Split(parts[1], ","),
|
return err
|
||||||
}); err != nil {
|
|
||||||
cclog.Abortf("Add User: Could not add new user authentication for '%s' and roles '%s'.\nError: %s\n", parts[0], parts[1], err.Error())
|
|
||||||
} else {
|
|
||||||
cclog.Printf("Add User: Added new user '%s' with roles '%s'.\n", parts[0], parts[1])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagDelUser != "" {
|
if flagDelUser != "" {
|
||||||
ur := repository.GetUserRepository()
|
if err := delUser(flagDelUser); err != nil {
|
||||||
if err := ur.DelUser(flagDelUser); err != nil {
|
return err
|
||||||
cclog.Abortf("Delete User: Could not delete user '%s' from DB.\nError: %s\n", flagDelUser, err.Error())
|
|
||||||
} else {
|
|
||||||
cclog.Printf("Delete User: Deleted user '%s' from DB.\n", flagDelUser)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authHandle := auth.GetAuthInstance()
|
authHandle := auth.GetAuthInstance()
|
||||||
|
|
||||||
if flagSyncLDAP {
|
if flagSyncLDAP {
|
||||||
if authHandle.LdapAuth == nil {
|
if err := syncLDAP(authHandle); err != nil {
|
||||||
cclog.Abort("Sync LDAP: LDAP authentication is not configured, could not synchronize. No changes, exited.")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := authHandle.LdapAuth.Sync(); err != nil {
|
|
||||||
cclog.Abortf("Sync LDAP: Could not synchronize, failed with error.\nError: %s\n", err.Error())
|
|
||||||
}
|
|
||||||
cclog.Print("Sync LDAP: LDAP synchronization successfull.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagGenJWT != "" {
|
if flagGenJWT != "" {
|
||||||
|
if err := generateJWT(authHandle, flagGenJWT); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDefaultSecurityKeys warns if default JWT keys are detected
|
||||||
|
func checkDefaultSecurityKeys() {
|
||||||
|
// Default JWT public key from init.go
|
||||||
|
defaultJWTPublic := "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
|
||||||
|
|
||||||
|
if os.Getenv("JWT_PUBLIC_KEY") == defaultJWTPublic {
|
||||||
|
cclog.Warn("Using default JWT keys - not recommended for production environments")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUser(userSpec string) error {
|
||||||
|
parts := strings.SplitN(userSpec, ":", 3)
|
||||||
|
if len(parts) != 3 || len(parts[0]) == 0 {
|
||||||
|
return fmt.Errorf("invalid user format, want: <username>:[admin,support,manager,api,user]:<password>, have: %s", userSpec)
|
||||||
|
}
|
||||||
|
|
||||||
ur := repository.GetUserRepository()
|
ur := repository.GetUserRepository()
|
||||||
user, err := ur.GetUser(flagGenJWT)
|
if err := ur.AddUser(&schema.User{
|
||||||
|
Username: parts[0],
|
||||||
|
Projects: make([]string, 0),
|
||||||
|
Password: parts[2],
|
||||||
|
Roles: strings.Split(parts[1], ","),
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("adding user '%s' with roles '%s': %w", parts[0], parts[1], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cclog.Printf("Add User: Added new user '%s' with roles '%s'.\n", parts[0], parts[1])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func delUser(username string) error {
|
||||||
|
ur := repository.GetUserRepository()
|
||||||
|
if err := ur.DelUser(username); err != nil {
|
||||||
|
return fmt.Errorf("deleting user '%s': %w", username, err)
|
||||||
|
}
|
||||||
|
cclog.Printf("Delete User: Deleted user '%s' from DB.\n", username)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncLDAP(authHandle *auth.Authentication) error {
|
||||||
|
if authHandle.LdapAuth == nil {
|
||||||
|
return fmt.Errorf("LDAP authentication is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := authHandle.LdapAuth.Sync(); err != nil {
|
||||||
|
return fmt.Errorf("synchronizing LDAP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cclog.Print("Sync LDAP: LDAP synchronization successfull.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateJWT(authHandle *auth.Authentication, username string) error {
|
||||||
|
ur := repository.GetUserRepository()
|
||||||
|
user, err := ur.GetUser(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Abortf("JWT: Could not get supplied user '%s' from DB. No changes, exited.\nError: %s\n", flagGenJWT, err.Error())
|
return fmt.Errorf("getting user '%s': %w", username, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.HasRole(schema.RoleApi) {
|
if !user.HasRole(schema.RoleApi) {
|
||||||
@@ -190,104 +258,222 @@ func main() {
|
|||||||
|
|
||||||
jwt, err := authHandle.JwtAuth.ProvideJWT(user)
|
jwt, err := authHandle.JwtAuth.ProvideJWT(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Abortf("JWT: User '%s' found in DB, but failed to provide JWT.\nError: %s\n", user.Username, err.Error())
|
return fmt.Errorf("generating JWT for user '%s': %w", user.Username, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cclog.Printf("JWT: Successfully generated JWT for user '%s': %s\n", user.Username, jwt)
|
cclog.Printf("JWT: Successfully generated JWT for user '%s': %s\n", user.Username, jwt)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSubsystems() error {
|
||||||
|
// Initialize archive
|
||||||
|
archiveCfg := ccconf.GetPackageConfig("archive")
|
||||||
|
if archiveCfg == nil {
|
||||||
|
archiveCfg = json.RawMessage(defaultArchiveConfig)
|
||||||
|
}
|
||||||
|
if err := archive.Init(archiveCfg, config.Keys.DisableArchive); err != nil {
|
||||||
|
return fmt.Errorf("initializing archive: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if flagNewUser != "" || flagDelUser != "" {
|
// Initialize metricdata
|
||||||
cclog.Abort("Error: Arguments '--add-user' and '--del-user' can only be used if authentication is enabled. No changes, exited.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if archiveCfg := ccconf.GetPackageConfig("archive"); archiveCfg != nil {
|
|
||||||
err = archive.Init(archiveCfg, config.Keys.DisableArchive)
|
|
||||||
} else {
|
|
||||||
err = archive.Init(json.RawMessage("{\"kind\":\"file\",\"path\":\"./var/job-archive\"}"), config.Keys.DisableArchive)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
cclog.Abortf("Init: Failed to initialize archive.\nError: %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := metricdata.Init(); err != nil {
|
if err := metricdata.Init(); err != nil {
|
||||||
cclog.Abortf("Init: Failed to initialize metricdata repository.\nError %s\n", err.Error())
|
return fmt.Errorf("initializing metricdata repository: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle database re-initialization
|
||||||
if flagReinitDB {
|
if flagReinitDB {
|
||||||
if err := importer.InitDB(); err != nil {
|
if err := importer.InitDB(); err != nil {
|
||||||
cclog.Abortf("Init DB: Failed to re-initialize repository DB.\nError: %s\n", err.Error())
|
return fmt.Errorf("re-initializing repository DB: %w", err)
|
||||||
} else {
|
|
||||||
cclog.Print("Init DB: Sucessfully re-initialized repository DB.")
|
|
||||||
}
|
}
|
||||||
|
cclog.Print("Init DB: Successfully re-initialized repository DB.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle job import
|
||||||
if flagImportJob != "" {
|
if flagImportJob != "" {
|
||||||
if err := importer.HandleImportFlag(flagImportJob); err != nil {
|
if err := importer.HandleImportFlag(flagImportJob); err != nil {
|
||||||
cclog.Abortf("Import Job: Job import failed.\nError: %s\n", err.Error())
|
return fmt.Errorf("importing job: %w", err)
|
||||||
} else {
|
}
|
||||||
cclog.Printf("Import Job: Imported Job '%s' into DB.\n", flagImportJob)
|
cclog.Printf("Import Job: Imported Job '%s' into DB.\n", flagImportJob)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Initialize taggers
|
||||||
if config.Keys.EnableJobTaggers {
|
if config.Keys.EnableJobTaggers {
|
||||||
tagger.Init()
|
tagger.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply tags if requested
|
||||||
if flagApplyTags {
|
if flagApplyTags {
|
||||||
if err := tagger.RunTaggers(); err != nil {
|
if err := tagger.RunTaggers(); err != nil {
|
||||||
cclog.Abortf("Running job taggers.\nError: %s\n", err.Error())
|
return fmt.Errorf("running job taggers: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !flagServer {
|
return nil
|
||||||
cclog.Exit("No errors, server flag not set. Exiting cc-backend.")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
func runServer(ctx context.Context) error {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// Metric Store starts after all flags have been processes
|
// Start metric store if enabled
|
||||||
if memorystore.InternalCCMSFlag {
|
if memorystore.InternalCCMSFlag {
|
||||||
if mscfg := ccconf.GetPackageConfig("metric-store"); mscfg != nil {
|
mscfg := ccconf.GetPackageConfig("metric-store")
|
||||||
|
if mscfg == nil {
|
||||||
|
return fmt.Errorf("metric store configuration must be present")
|
||||||
|
}
|
||||||
memorystore.Init(mscfg, &wg)
|
memorystore.Init(mscfg, &wg)
|
||||||
} else {
|
|
||||||
cclog.Abort("Metric Store configuration must be present")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start archiver and task manager
|
||||||
archiver.Start(repository.GetJobRepository())
|
archiver.Start(repository.GetJobRepository())
|
||||||
|
taskManager.Start(ccconf.GetPackageConfig("cron"), ccconf.GetPackageConfig("archive"))
|
||||||
|
|
||||||
taskManager.Start(ccconf.GetPackageConfig("cron"),
|
// Initialize web UI
|
||||||
ccconf.GetPackageConfig("archive"))
|
|
||||||
|
|
||||||
cfg := ccconf.GetPackageConfig("ui")
|
cfg := ccconf.GetPackageConfig("ui")
|
||||||
web.Init(cfg)
|
web.Init(cfg)
|
||||||
|
|
||||||
serverInit()
|
// Initialize HTTP server
|
||||||
|
srv, err := NewServer(version, commit, date)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel to collect errors from server
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
// Start HTTP server
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
serverStart()
|
if err := srv.Start(ctx); err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Handle shutdown signals
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
<-sigs
|
select {
|
||||||
|
case <-sigs:
|
||||||
|
cclog.Info("Shutdown signal received")
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
|
||||||
runtimeEnv.SystemdNotifiy(false, "Shutting down ...")
|
runtimeEnv.SystemdNotifiy(false, "Shutting down ...")
|
||||||
|
srv.Shutdown(ctx)
|
||||||
serverShutdown()
|
|
||||||
|
|
||||||
util.FsWatcherShutdown()
|
util.FsWatcherShutdown()
|
||||||
|
|
||||||
taskManager.Shutdown()
|
taskManager.Shutdown()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if os.Getenv("GOGC") == "" {
|
// Set GC percent if not configured
|
||||||
|
if os.Getenv(envGOGC) == "" {
|
||||||
debug.SetGCPercent(25)
|
debug.SetGCPercent(25)
|
||||||
}
|
}
|
||||||
runtimeEnv.SystemdNotifiy(true, "running")
|
runtimeEnv.SystemdNotifiy(true, "running")
|
||||||
|
|
||||||
|
// Wait for completion or error
|
||||||
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
close(errChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Check for server startup errors
|
||||||
|
select {
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
// Server started successfully, wait for completion
|
||||||
|
if err := <-errChan; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cclog.Print("Graceful shutdown completed!")
|
cclog.Print("Graceful shutdown completed!")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
cliInit()
|
||||||
|
|
||||||
|
// Handle version flag
|
||||||
|
if flagVersion {
|
||||||
|
printVersion()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
cclog.Init(flagLogLevel, flagLogDateTime)
|
||||||
|
|
||||||
|
// Handle init flag
|
||||||
|
if flagInit {
|
||||||
|
initEnv()
|
||||||
|
cclog.Exit("Successfully setup environment!\n" +
|
||||||
|
"Please review config.json and .env and adjust it to your needs.\n" +
|
||||||
|
"Add your job-archive at ./var/job-archive.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize gops agent
|
||||||
|
if err := initGops(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize subsystems in dependency order:
|
||||||
|
// 1. Load environment variables from .env file (contains sensitive configuration)
|
||||||
|
// 2. Load configuration from config.json (may reference environment variables)
|
||||||
|
// 3. Initialize database connection (requires config for connection string)
|
||||||
|
// 4. Handle database commands if requested (requires active database connection)
|
||||||
|
// 5. Handle user commands if requested (requires database and authentication config)
|
||||||
|
// 6. Initialize subsystems like archive and metrics (require config and database)
|
||||||
|
|
||||||
|
// Load environment and configuration
|
||||||
|
if err := loadEnvironment(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initConfiguration(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database
|
||||||
|
if err := initDatabase(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle database commands (migrate, revert, force)
|
||||||
|
if err := handleDatabaseCommands(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user commands (add, delete, sync, JWT)
|
||||||
|
if err := handleUserCommands(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize subsystems (archive, metrics, taggers)
|
||||||
|
if err := initSubsystems(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server if requested
|
||||||
|
if !flagServer {
|
||||||
|
cclog.Exit("No errors, server flag not set. Exiting cc-backend.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run server with context
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return runServer(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
cclog.Error(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
// All rights reserved. This file is part of cc-backend.
|
// All rights reserved. This file is part of cc-backend.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
// Package main provides the entry point for the ClusterCockpit backend server.
|
||||||
|
// This file contains HTTP server setup, routing configuration, and
|
||||||
|
// authentication middleware integration.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -37,10 +40,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
buildInfo web.Build
|
||||||
|
)
|
||||||
|
|
||||||
|
// Environment variable names
|
||||||
|
const (
|
||||||
|
envDebug = "DEBUG"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server encapsulates the HTTP server state and dependencies
|
||||||
|
type Server struct {
|
||||||
router *mux.Router
|
router *mux.Router
|
||||||
server *http.Server
|
server *http.Server
|
||||||
apiHandle *api.RestApi
|
apiHandle *api.RestApi
|
||||||
)
|
}
|
||||||
|
|
||||||
func onFailureResponse(rw http.ResponseWriter, r *http.Request, err error) {
|
func onFailureResponse(rw http.ResponseWriter, r *http.Request, err error) {
|
||||||
rw.Header().Add("Content-Type", "application/json")
|
rw.Header().Add("Content-Type", "application/json")
|
||||||
@@ -51,25 +64,31 @@ func onFailureResponse(rw http.ResponseWriter, r *http.Request, err error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serverInit() {
|
// NewServer creates and initializes a new Server instance
|
||||||
|
func NewServer(version, commit, buildDate string) (*Server, error) {
|
||||||
|
buildInfo = web.Build{Version: version, Hash: commit, Buildtime: buildDate}
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
router: mux.NewRouter(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) init() error {
|
||||||
// Setup the http.Handler/Router used by the server
|
// Setup the http.Handler/Router used by the server
|
||||||
graph.Init()
|
graph.Init()
|
||||||
resolver := graph.GetResolverInstance()
|
resolver := graph.GetResolverInstance()
|
||||||
graphQLServer := handler.New(
|
graphQLServer := handler.New(
|
||||||
generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))
|
generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))
|
||||||
|
|
||||||
// graphQLServer.AddTransport(transport.SSE{})
|
|
||||||
graphQLServer.AddTransport(transport.POST{})
|
graphQLServer.AddTransport(transport.POST{})
|
||||||
// graphQLServer.AddTransport(transport.Websocket{
|
|
||||||
// KeepAlivePingInterval: 10 * time.Second,
|
|
||||||
// Upgrader: websocket.Upgrader{
|
|
||||||
// CheckOrigin: func(r *http.Request) bool {
|
|
||||||
// return true
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
if os.Getenv("DEBUG") != "1" {
|
if os.Getenv(envDebug) != "1" {
|
||||||
// Having this handler means that a error message is returned via GraphQL instead of the connection simply beeing closed.
|
// Having this handler means that a error message is returned via GraphQL instead of the connection simply beeing closed.
|
||||||
// The problem with this is that then, no more stacktrace is printed to stderr.
|
// The problem with this is that then, no more stacktrace is printed to stderr.
|
||||||
graphQLServer.SetRecoverFunc(func(ctx context.Context, err any) error {
|
graphQLServer.SetRecoverFunc(func(ctx context.Context, err any) error {
|
||||||
@@ -86,46 +105,41 @@ func serverInit() {
|
|||||||
|
|
||||||
authHandle := auth.GetAuthInstance()
|
authHandle := auth.GetAuthInstance()
|
||||||
|
|
||||||
apiHandle = api.New()
|
s.apiHandle = api.New()
|
||||||
|
|
||||||
router = mux.NewRouter()
|
|
||||||
buildInfo := web.Build{Version: version, Hash: commit, Buildtime: date}
|
|
||||||
|
|
||||||
info := map[string]any{}
|
info := map[string]any{}
|
||||||
info["hasOpenIDConnect"] = false
|
info["hasOpenIDConnect"] = false
|
||||||
|
|
||||||
if auth.Keys.OpenIDConfig != nil {
|
if auth.Keys.OpenIDConfig != nil {
|
||||||
openIDConnect := auth.NewOIDC(authHandle)
|
openIDConnect := auth.NewOIDC(authHandle)
|
||||||
openIDConnect.RegisterEndpoints(router)
|
openIDConnect.RegisterEndpoints(s.router)
|
||||||
info["hasOpenIDConnect"] = true
|
info["hasOpenIDConnect"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
router.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
|
s.router.HandleFunc("/login", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
cclog.Debugf("##%v##", info)
|
cclog.Debugf("##%v##", info)
|
||||||
web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo, Infos: info})
|
web.RenderTemplate(rw, "login.tmpl", &web.Page{Title: "Login", Build: buildInfo, Infos: info})
|
||||||
}).Methods(http.MethodGet)
|
}).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
|
s.router.HandleFunc("/imprint", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
web.RenderTemplate(rw, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo})
|
web.RenderTemplate(rw, "imprint.tmpl", &web.Page{Title: "Imprint", Build: buildInfo})
|
||||||
})
|
})
|
||||||
router.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
|
s.router.HandleFunc("/privacy", func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
|
web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
|
||||||
})
|
})
|
||||||
|
|
||||||
secured := router.PathPrefix("/").Subrouter()
|
secured := s.router.PathPrefix("/").Subrouter()
|
||||||
securedapi := router.PathPrefix("/api").Subrouter()
|
securedapi := s.router.PathPrefix("/api").Subrouter()
|
||||||
userapi := router.PathPrefix("/userapi").Subrouter()
|
userapi := s.router.PathPrefix("/userapi").Subrouter()
|
||||||
configapi := router.PathPrefix("/config").Subrouter()
|
configapi := s.router.PathPrefix("/config").Subrouter()
|
||||||
frontendapi := router.PathPrefix("/frontend").Subrouter()
|
frontendapi := s.router.PathPrefix("/frontend").Subrouter()
|
||||||
metricstoreapi := router.PathPrefix("/metricstore").Subrouter()
|
metricstoreapi := s.router.PathPrefix("/metricstore").Subrouter()
|
||||||
|
|
||||||
if !config.Keys.DisableAuthentication {
|
if !config.Keys.DisableAuthentication {
|
||||||
router.Handle("/login", authHandle.Login(
|
// Create login failure handler (used by both /login and /jwt-login)
|
||||||
// On success: Handled within Login()
|
loginFailureHandler := func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||||
// On failure:
|
|
||||||
func(rw http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
rw.WriteHeader(http.StatusUnauthorized)
|
rw.WriteHeader(http.StatusUnauthorized)
|
||||||
web.RenderTemplate(rw, "login.tmpl", &web.Page{
|
web.RenderTemplate(rw, "login.tmpl", &web.Page{
|
||||||
@@ -135,24 +149,12 @@ func serverInit() {
|
|||||||
Build: buildInfo,
|
Build: buildInfo,
|
||||||
Infos: info,
|
Infos: info,
|
||||||
})
|
})
|
||||||
})).Methods(http.MethodPost)
|
}
|
||||||
|
|
||||||
router.Handle("/jwt-login", authHandle.Login(
|
s.router.Handle("/login", authHandle.Login(loginFailureHandler)).Methods(http.MethodPost)
|
||||||
// On success: Handled within Login()
|
s.router.Handle("/jwt-login", authHandle.Login(loginFailureHandler))
|
||||||
// On failure:
|
|
||||||
func(rw http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
|
||||||
rw.WriteHeader(http.StatusUnauthorized)
|
|
||||||
web.RenderTemplate(rw, "login.tmpl", &web.Page{
|
|
||||||
Title: "Login failed - ClusterCockpit",
|
|
||||||
MsgType: "alert-warning",
|
|
||||||
Message: err.Error(),
|
|
||||||
Build: buildInfo,
|
|
||||||
Infos: info,
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
router.Handle("/logout", authHandle.Logout(
|
s.router.Handle("/logout", authHandle.Logout(
|
||||||
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
@@ -226,8 +228,8 @@ func serverInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if flagDev {
|
if flagDev {
|
||||||
router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
|
s.router.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
|
||||||
router.PathPrefix("/swagger/").Handler(httpSwagger.Handler(
|
s.router.PathPrefix("/swagger/").Handler(httpSwagger.Handler(
|
||||||
httpSwagger.URL("http://" + config.Keys.Addr + "/swagger/doc.json"))).Methods(http.MethodGet)
|
httpSwagger.URL("http://" + config.Keys.Addr + "/swagger/doc.json"))).Methods(http.MethodGet)
|
||||||
}
|
}
|
||||||
secured.Handle("/query", graphQLServer)
|
secured.Handle("/query", graphQLServer)
|
||||||
@@ -239,67 +241,47 @@ func serverInit() {
|
|||||||
|
|
||||||
// Mount all /monitoring/... and /api/... routes.
|
// Mount all /monitoring/... and /api/... routes.
|
||||||
routerConfig.SetupRoutes(secured, buildInfo)
|
routerConfig.SetupRoutes(secured, buildInfo)
|
||||||
apiHandle.MountApiRoutes(securedapi)
|
s.apiHandle.MountApiRoutes(securedapi)
|
||||||
apiHandle.MountUserApiRoutes(userapi)
|
s.apiHandle.MountUserApiRoutes(userapi)
|
||||||
apiHandle.MountConfigApiRoutes(configapi)
|
s.apiHandle.MountConfigApiRoutes(configapi)
|
||||||
apiHandle.MountFrontendApiRoutes(frontendapi)
|
s.apiHandle.MountFrontendApiRoutes(frontendapi)
|
||||||
|
|
||||||
if memorystore.InternalCCMSFlag {
|
if memorystore.InternalCCMSFlag {
|
||||||
apiHandle.MountMetricStoreApiRoutes(metricstoreapi)
|
s.apiHandle.MountMetricStoreApiRoutes(metricstoreapi)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Keys.EmbedStaticFiles {
|
if config.Keys.EmbedStaticFiles {
|
||||||
if i, err := os.Stat("./var/img"); err == nil {
|
if i, err := os.Stat("./var/img"); err == nil {
|
||||||
if i.IsDir() {
|
if i.IsDir() {
|
||||||
cclog.Info("Use local directory for static images")
|
cclog.Info("Use local directory for static images")
|
||||||
router.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("./var/img"))))
|
s.router.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("./var/img"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
router.PathPrefix("/").Handler(http.StripPrefix("/", web.ServeFiles()))
|
s.router.PathPrefix("/").Handler(http.StripPrefix("/", web.ServeFiles()))
|
||||||
} else {
|
} else {
|
||||||
router.PathPrefix("/").Handler(http.FileServer(http.Dir(config.Keys.StaticFiles)))
|
s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(config.Keys.StaticFiles)))
|
||||||
}
|
}
|
||||||
|
|
||||||
router.Use(handlers.CompressHandler)
|
s.router.Use(handlers.CompressHandler)
|
||||||
router.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
|
s.router.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
|
||||||
router.Use(handlers.CORS(
|
s.router.Use(handlers.CORS(
|
||||||
handlers.AllowCredentials(),
|
handlers.AllowCredentials(),
|
||||||
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization", "Origin"}),
|
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization", "Origin"}),
|
||||||
handlers.AllowedMethods([]string{"GET", "POST", "HEAD", "OPTIONS"}),
|
handlers.AllowedMethods([]string{"GET", "POST", "HEAD", "OPTIONS"}),
|
||||||
handlers.AllowedOrigins([]string{"*"})))
|
handlers.AllowedOrigins([]string{"*"})))
|
||||||
|
|
||||||
// secured.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
// page := web.Page{
|
|
||||||
// Title: "ClusterCockpit - Not Found",
|
|
||||||
// Build: buildInfo,
|
|
||||||
// }
|
|
||||||
// rw.Header().Add("Content-Type", "text/html; charset=utf-8")
|
|
||||||
// web.RenderTemplate(rw, "404.tmpl", &page)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// secured.NotFoundHandler = http.HandlerFunc(http.NotFound)
|
return nil
|
||||||
// router.NotFoundHandler = router.NewRoute().HandlerFunc(http.NotFound).GetHandler()
|
|
||||||
|
|
||||||
// printEndpoints(router)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// func printEndpoints(r *mux.Router) {
|
// Server timeout defaults (in seconds)
|
||||||
// r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
const (
|
||||||
// path, err := route.GetPathTemplate()
|
defaultReadTimeout = 20
|
||||||
// if err != nil {
|
defaultWriteTimeout = 20
|
||||||
// path = "nopath"
|
)
|
||||||
// }
|
|
||||||
// methods, err := route.GetMethods()
|
|
||||||
// if err != nil {
|
|
||||||
// methods = append(methods, "nomethod")
|
|
||||||
// }
|
|
||||||
// fmt.Printf("%v %s\n", methods, path)
|
|
||||||
// return nil
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
func serverStart() {
|
func (s *Server) Start(ctx context.Context) error {
|
||||||
handler := handlers.CustomLoggingHandler(io.Discard, router, func(_ io.Writer, params handlers.LogFormatterParams) {
|
handler := handlers.CustomLoggingHandler(io.Discard, s.router, func(_ io.Writer, params handlers.LogFormatterParams) {
|
||||||
if strings.HasPrefix(params.Request.RequestURI, "/api/") {
|
if strings.HasPrefix(params.Request.RequestURI, "/api/") {
|
||||||
cclog.Debugf("%s %s (%d, %.02fkb, %dms)",
|
cclog.Debugf("%s %s (%d, %.02fkb, %dms)",
|
||||||
params.Request.Method, params.URL.RequestURI(),
|
params.Request.Method, params.URL.RequestURI(),
|
||||||
@@ -313,9 +295,13 @@ func serverStart() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
server = &http.Server{
|
// Use configurable timeouts with defaults
|
||||||
ReadTimeout: 20 * time.Second,
|
readTimeout := time.Duration(defaultReadTimeout) * time.Second
|
||||||
WriteTimeout: 20 * time.Second,
|
writeTimeout := time.Duration(defaultWriteTimeout) * time.Second
|
||||||
|
|
||||||
|
s.server = &http.Server{
|
||||||
|
ReadTimeout: readTimeout,
|
||||||
|
WriteTimeout: writeTimeout,
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
Addr: config.Keys.Addr,
|
Addr: config.Keys.Addr,
|
||||||
}
|
}
|
||||||
@@ -323,7 +309,7 @@ func serverStart() {
|
|||||||
// Start http or https server
|
// Start http or https server
|
||||||
listener, err := net.Listen("tcp", config.Keys.Addr)
|
listener, err := net.Listen("tcp", config.Keys.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Abortf("Server Start: Starting http listener on '%s' failed.\nError: %s\n", config.Keys.Addr, err.Error())
|
return fmt.Errorf("starting listener on '%s': %w", config.Keys.Addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasSuffix(config.Keys.Addr, ":80") && config.Keys.RedirectHTTPTo != "" {
|
if !strings.HasSuffix(config.Keys.Addr, ":80") && config.Keys.RedirectHTTPTo != "" {
|
||||||
@@ -336,7 +322,7 @@ func serverStart() {
|
|||||||
cert, err := tls.LoadX509KeyPair(
|
cert, err := tls.LoadX509KeyPair(
|
||||||
config.Keys.HTTPSCertFile, config.Keys.HTTPSKeyFile)
|
config.Keys.HTTPSCertFile, config.Keys.HTTPSKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Abortf("Server Start: Loading X509 keypair failed. Check options 'https-cert-file' and 'https-key-file' in 'config.json'.\nError: %s\n", err.Error())
|
return fmt.Errorf("loading X509 keypair (check 'https-cert-file' and 'https-key-file' in config.json): %w", err)
|
||||||
}
|
}
|
||||||
listener = tls.NewListener(listener, &tls.Config{
|
listener = tls.NewListener(listener, &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
@@ -356,17 +342,34 @@ func serverStart() {
|
|||||||
// be established first, then the user can be changed, and after that,
|
// be established first, then the user can be changed, and after that,
|
||||||
// the actual http server can be started.
|
// the actual http server can be started.
|
||||||
if err := runtimeEnv.DropPrivileges(config.Keys.Group, config.Keys.User); err != nil {
|
if err := runtimeEnv.DropPrivileges(config.Keys.Group, config.Keys.User); err != nil {
|
||||||
cclog.Abortf("Server Start: Error while preparing server start.\nError: %s\n", err.Error())
|
return fmt.Errorf("dropping privileges: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
// Handle context cancellation for graceful shutdown
|
||||||
cclog.Abortf("Server Start: Starting server failed.\nError: %s\n", err.Error())
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := s.server.Shutdown(shutdownCtx); err != nil {
|
||||||
|
cclog.Errorf("Server shutdown error: %v", err)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||||
|
return fmt.Errorf("server failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func serverShutdown() {
|
func (s *Server) Shutdown(ctx context.Context) {
|
||||||
|
// Create a shutdown context with timeout
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// First shut down the server gracefully (waiting for all ongoing requests)
|
// First shut down the server gracefully (waiting for all ongoing requests)
|
||||||
server.Shutdown(context.Background())
|
if err := s.server.Shutdown(shutdownCtx); err != nil {
|
||||||
|
cclog.Errorf("Server shutdown error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Archive all the metric store data
|
// Archive all the metric store data
|
||||||
if memorystore.InternalCCMSFlag {
|
if memorystore.InternalCCMSFlag {
|
||||||
|
|||||||
Reference in New Issue
Block a user