mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
421 lines
12 KiB
Go
421 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"syscall"
|
|
"time"
|
|
|
|
"databasus-backend/internal/config"
|
|
"databasus-backend/internal/features/audit_logs"
|
|
"databasus-backend/internal/features/backups/backups/backuping"
|
|
backups_controllers "databasus-backend/internal/features/backups/backups/controllers"
|
|
backups_download "databasus-backend/internal/features/backups/backups/download"
|
|
backups_services "databasus-backend/internal/features/backups/backups/services"
|
|
backups_config "databasus-backend/internal/features/backups/config"
|
|
"databasus-backend/internal/features/databases"
|
|
"databasus-backend/internal/features/disk"
|
|
"databasus-backend/internal/features/encryption/secrets"
|
|
healthcheck_attempt "databasus-backend/internal/features/healthcheck/attempt"
|
|
healthcheck_config "databasus-backend/internal/features/healthcheck/config"
|
|
"databasus-backend/internal/features/notifiers"
|
|
"databasus-backend/internal/features/restores"
|
|
"databasus-backend/internal/features/restores/restoring"
|
|
"databasus-backend/internal/features/storages"
|
|
system_healthcheck "databasus-backend/internal/features/system/healthcheck"
|
|
task_cancellation "databasus-backend/internal/features/tasks/cancellation"
|
|
users_controllers "databasus-backend/internal/features/users/controllers"
|
|
users_middleware "databasus-backend/internal/features/users/middleware"
|
|
users_services "databasus-backend/internal/features/users/services"
|
|
workspaces_controllers "databasus-backend/internal/features/workspaces/controllers"
|
|
cache_utils "databasus-backend/internal/util/cache"
|
|
env_utils "databasus-backend/internal/util/env"
|
|
files_utils "databasus-backend/internal/util/files"
|
|
"databasus-backend/internal/util/logger"
|
|
_ "databasus-backend/swagger" // swagger docs
|
|
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-contrib/gzip"
|
|
"github.com/gin-gonic/gin"
|
|
swaggerFiles "github.com/swaggo/files"
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
|
)
|
|
|
|
// @title Databasus Backend API
|
|
// @version 1.0
|
|
// @description API for Databasus
|
|
// @termsOfService http://swagger.io/terms/
|
|
|
|
// @host localhost:4005
|
|
// @BasePath /api/v1
|
|
// @schemes http
|
|
func main() {
|
|
log := logger.GetLogger()
|
|
|
|
cache_utils.TestCacheConnection()
|
|
|
|
if config.GetEnv().IsPrimaryNode {
|
|
log.Info("Clearing cache...")
|
|
|
|
err := cache_utils.ClearAllCache()
|
|
if err != nil {
|
|
log.Error("Failed to clear cache", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if config.GetEnv().IsPrimaryNode {
|
|
runMigrations(log)
|
|
} else {
|
|
log.Info("Skipping migrations (IS_PRIMARY_NODE is false)")
|
|
}
|
|
|
|
// create directories that used for backups and restore
|
|
err := files_utils.EnsureDirectories([]string{
|
|
config.GetEnv().TempFolder,
|
|
config.GetEnv().DataFolder,
|
|
})
|
|
|
|
if err != nil {
|
|
log.Error("Failed to ensure directories", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
err = secrets.GetSecretKeyService().MigrateKeyFromDbToFileIfExist()
|
|
if err != nil {
|
|
log.Error("Failed to migrate secret key from database to file", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
err = users_services.GetUserService().CreateInitialAdmin()
|
|
if err != nil {
|
|
log.Error("Failed to create initial admin", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
handlePasswordReset(log)
|
|
|
|
go generateSwaggerDocs(log)
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
ginApp := gin.Default()
|
|
|
|
// Add GZIP compression middleware
|
|
ginApp.Use(gzip.Gzip(
|
|
gzip.DefaultCompression,
|
|
// Don't compress already compressed files
|
|
gzip.WithExcludedExtensions(
|
|
[]string{".png", ".gif", ".jpeg", ".jpg", ".ico", ".svg", ".pdf", ".mp4"},
|
|
),
|
|
))
|
|
|
|
enableCors(ginApp)
|
|
setUpRoutes(ginApp)
|
|
setUpDependencies()
|
|
|
|
runBackgroundTasks(log)
|
|
|
|
mountFrontend(ginApp)
|
|
|
|
startServerWithGracefulShutdown(log, ginApp)
|
|
}
|
|
|
|
func handlePasswordReset(log *slog.Logger) {
|
|
audit_logs.SetupDependencies()
|
|
|
|
newPassword := flag.String("new-password", "", "Set a new password for the user")
|
|
email := flag.String("email", "", "Email of the user to reset password")
|
|
|
|
flag.Parse()
|
|
|
|
if *newPassword == "" {
|
|
return
|
|
}
|
|
|
|
log.Info("Found reset password command - reseting password...")
|
|
|
|
if *email == "" {
|
|
log.Info("No email provided, please provide an email via --email=\"some@email.com\" flag")
|
|
os.Exit(1)
|
|
}
|
|
|
|
resetPassword(*email, *newPassword, log)
|
|
}
|
|
|
|
func resetPassword(email string, newPassword string, log *slog.Logger) {
|
|
log.Info("Resetting password...")
|
|
|
|
userService := users_services.GetUserService()
|
|
err := userService.ChangeUserPasswordByEmail(email, newPassword)
|
|
if err != nil {
|
|
log.Error("Failed to reset password", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
log.Info("Password reset successfully")
|
|
os.Exit(0)
|
|
}
|
|
|
|
func startServerWithGracefulShutdown(log *slog.Logger, app *gin.Engine) {
|
|
host := ""
|
|
if config.GetEnv().EnvMode == env_utils.EnvModeDevelopment {
|
|
// for dev we use localhost to avoid firewall
|
|
// requests on each run for Windows
|
|
host = "127.0.0.1"
|
|
}
|
|
|
|
srv := &http.Server{
|
|
Addr: host + ":4005",
|
|
Handler: app,
|
|
}
|
|
|
|
go func() {
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Error("listen:", "error", err)
|
|
}
|
|
}()
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
|
<-quit
|
|
log.Info("Shutdown signal received")
|
|
|
|
// Gracefully shutdown VictoriaLogs writer
|
|
logger.ShutdownVictoriaLogs(5 * time.Second)
|
|
|
|
// The context is used to inform the server it has 10 seconds to finish
|
|
// the request it is currently handling
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
log.Error("Server forced to shutdown:", "error", err)
|
|
}
|
|
|
|
log.Info("Server gracefully stopped")
|
|
}
|
|
|
|
func setUpRoutes(r *gin.Engine) {
|
|
v1 := r.Group("/api/v1")
|
|
|
|
// Mount Swagger UI
|
|
v1.GET("/docs/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
|
|
// Public routes (only user auth routes and healthcheck should be public)
|
|
userController := users_controllers.GetUserController()
|
|
userController.RegisterRoutes(v1)
|
|
system_healthcheck.GetHealthcheckController().RegisterRoutes(v1)
|
|
backups_controllers.GetBackupController().RegisterPublicRoutes(v1)
|
|
backups_controllers.GetPostgresWalBackupController().RegisterRoutes(v1)
|
|
databases.GetDatabaseController().RegisterPublicRoutes(v1)
|
|
|
|
// Setup auth middleware
|
|
userService := users_services.GetUserService()
|
|
authMiddleware := users_middleware.AuthMiddleware(userService)
|
|
|
|
// Protected routes
|
|
protected := v1.Group("")
|
|
protected.Use(authMiddleware)
|
|
|
|
userController.RegisterProtectedRoutes(protected)
|
|
workspaces_controllers.GetWorkspaceController().RegisterRoutes(protected)
|
|
workspaces_controllers.GetMembershipController().RegisterRoutes(protected)
|
|
disk.GetDiskController().RegisterRoutes(protected)
|
|
notifiers.GetNotifierController().RegisterRoutes(protected)
|
|
storages.GetStorageController().RegisterRoutes(protected)
|
|
databases.GetDatabaseController().RegisterRoutes(protected)
|
|
backups_controllers.GetBackupController().RegisterRoutes(protected)
|
|
restores.GetRestoreController().RegisterRoutes(protected)
|
|
healthcheck_config.GetHealthcheckConfigController().RegisterRoutes(protected)
|
|
healthcheck_attempt.GetHealthcheckAttemptController().RegisterRoutes(protected)
|
|
backups_config.GetBackupConfigController().RegisterRoutes(protected)
|
|
audit_logs.GetAuditLogController().RegisterRoutes(protected)
|
|
users_controllers.GetManagementController().RegisterRoutes(protected)
|
|
users_controllers.GetSettingsController().RegisterRoutes(protected)
|
|
}
|
|
|
|
func setUpDependencies() {
|
|
databases.SetupDependencies()
|
|
backups_services.SetupDependencies()
|
|
restores.SetupDependencies()
|
|
healthcheck_config.SetupDependencies()
|
|
audit_logs.SetupDependencies()
|
|
notifiers.SetupDependencies()
|
|
storages.SetupDependencies()
|
|
backups_config.SetupDependencies()
|
|
task_cancellation.SetupDependencies()
|
|
}
|
|
|
|
func runBackgroundTasks(log *slog.Logger) {
|
|
log.Info("Preparing to run background tasks...")
|
|
|
|
// Create context that will be cancelled on shutdown
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Set up signal handling for graceful shutdown
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-quit
|
|
log.Info("Shutdown signal received, cancelling all background tasks")
|
|
cancel()
|
|
}()
|
|
|
|
err := files_utils.CleanFolder(config.GetEnv().TempFolder)
|
|
if err != nil {
|
|
log.Error("Failed to clean temp folder", "error", err)
|
|
}
|
|
|
|
if config.GetEnv().IsPrimaryNode {
|
|
log.Info("Starting primary node background tasks...")
|
|
|
|
go runWithPanicLogging(log, "backup background service", func() {
|
|
backuping.GetBackupsScheduler().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "backup cleaner background service", func() {
|
|
backuping.GetBackupCleaner().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "restore background service", func() {
|
|
restoring.GetRestoresScheduler().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "healthcheck attempt background service", func() {
|
|
healthcheck_attempt.GetHealthcheckAttemptBackgroundService().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "audit log cleanup background service", func() {
|
|
audit_logs.GetAuditLogBackgroundService().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "download token cleanup background service", func() {
|
|
backups_download.GetDownloadTokenBackgroundService().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "backup nodes registry background service", func() {
|
|
backuping.GetBackupNodesRegistry().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "restore nodes registry background service", func() {
|
|
restoring.GetRestoreNodesRegistry().Run(ctx)
|
|
})
|
|
} else {
|
|
log.Info("Skipping primary node tasks as not primary node")
|
|
}
|
|
|
|
if config.GetEnv().IsProcessingNode {
|
|
log.Info("Starting backup node background tasks...")
|
|
|
|
go runWithPanicLogging(log, "backup node", func() {
|
|
backuping.GetBackuperNode().Run(ctx)
|
|
})
|
|
|
|
go runWithPanicLogging(log, "restore node", func() {
|
|
restoring.GetRestorerNode().Run(ctx)
|
|
})
|
|
} else {
|
|
log.Info("Skipping backup/restore node tasks as not backup node")
|
|
}
|
|
}
|
|
|
|
func runWithPanicLogging(log *slog.Logger, serviceName string, fn func()) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Error("Panic in "+serviceName, "error", r)
|
|
}
|
|
}()
|
|
fn()
|
|
}
|
|
|
|
// Keep in mind: docs appear after second launch, because Swagger
|
|
// is generated into Go files. So if we changed files, we generate
|
|
// new docs, but still need to restart the server to see them.
|
|
func generateSwaggerDocs(log *slog.Logger) {
|
|
if config.GetEnv().EnvMode == env_utils.EnvModeProduction {
|
|
return
|
|
}
|
|
|
|
// Run swag from the current directory instead of parent
|
|
// Use the current directory as the base for swag init
|
|
// This ensures swag can find the files regardless of where the command is run from
|
|
currentDir, err := os.Getwd()
|
|
if err != nil {
|
|
log.Error("Failed to get current directory", "error", err)
|
|
return
|
|
}
|
|
|
|
cmd := exec.Command("swag", "init", "-d", currentDir, "-g", "cmd/main.go", "-o", "swagger")
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Error("Failed to generate Swagger docs", "error", err, "output", string(output))
|
|
return
|
|
}
|
|
|
|
log.Info("Swagger documentation generated successfully")
|
|
}
|
|
|
|
func runMigrations(log *slog.Logger) {
|
|
log.Info("Running database migrations...")
|
|
|
|
cmd := exec.Command("goose", "-dir", "./migrations", "up")
|
|
cmd.Env = append(
|
|
os.Environ(),
|
|
"GOOSE_DRIVER=postgres",
|
|
"GOOSE_DBSTRING="+config.GetEnv().DatabaseDsn,
|
|
)
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
log.Error("Failed to run migrations", "error", err, "output", string(output))
|
|
os.Exit(1)
|
|
}
|
|
|
|
log.Info("Database migrations completed successfully", "output", string(output))
|
|
}
|
|
|
|
func enableCors(ginApp *gin.Engine) {
|
|
if config.GetEnv().EnvMode == env_utils.EnvModeDevelopment {
|
|
// Setup CORS
|
|
ginApp.Use(cors.New(cors.Config{
|
|
AllowOrigins: []string{"*"},
|
|
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"},
|
|
AllowHeaders: []string{
|
|
"Origin",
|
|
"Content-Length",
|
|
"Content-Type",
|
|
"Authorization",
|
|
"Accept",
|
|
"Accept-Language",
|
|
"Accept-Encoding",
|
|
"Access-Control-Request-Method",
|
|
"Access-Control-Request-Headers",
|
|
"Access-Control-Allow-Methods",
|
|
"Access-Control-Allow-Headers",
|
|
"Access-Control-Allow-Origin",
|
|
},
|
|
AllowCredentials: true,
|
|
}))
|
|
}
|
|
}
|
|
|
|
func mountFrontend(ginApp *gin.Engine) {
|
|
staticDir := "./ui/build"
|
|
ginApp.NoRoute(func(c *gin.Context) {
|
|
path := filepath.Join(staticDir, c.Request.URL.Path)
|
|
|
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
|
c.File(path)
|
|
return
|
|
}
|
|
|
|
c.File(filepath.Join(staticDir, "index.html"))
|
|
})
|
|
}
|