package main import ( "context" "log" "os" "time" "github.com/gin-gonic/gin" "github.com/mrhid6/vantage/server/internal/api" "github.com/mrhid6/vantage/server/internal/auth" "github.com/mrhid6/vantage/server/internal/db" grpcserver "github.com/mrhid6/vantage/server/internal/grpc" "github.com/mrhid6/vantage/server/internal/monitorsched" "github.com/mrhid6/vantage/server/internal/services" ) func main() { mongoURI := getEnv("MONGO_URI", "mongodb://localhost:27017") dbName := getEnv("MONGO_DB", "vantage") if os.Getenv("GRPC_HOST") == "" { log.Fatal("GRPC_HOST is required (host:port agents dial for gRPC)") } if err := db.Connect(mongoURI, dbName); err != nil { log.Fatalf("failed to connect to MongoDB: %v", err) } log.Println("connected to MongoDB") // Migrations 0001 to 0003 still speak the pre-rename shape (orgs, org_id), // so they must run before 0004 renames everything underneath them. if err := services.RunMigrations(); err != nil { log.Fatalf("migration failed: %v", err) } // 0002 must precede 0003: 0003 can create a "default" org, which pushes // 0002 into its ambiguous multi-org branch. if err := services.MigrateSettingsOrg(); err != nil { log.Fatalf("settings org migration failed: %v", err) } if err := services.MigrateMissedOrgScopes(); err != nil { log.Fatalf("missed org scope migration failed: %v", err) } // 0004 renames orgs to instances. It must run BEFORE the index builders: // EnsureAuthIndexes creates instances.slug, which would create an empty // instances collection and make 0004 refuse to rename onto it. migCtx, migCancel := context.WithTimeout(context.Background(), 10*time.Minute) migErr := services.MigrateOrgToInstance(migCtx, db.Database) migCancel() if migErr != nil { log.Fatalf("instance rename migration failed: %v", migErr) } assertCtx, assertCancel := context.WithTimeout(context.Background(), 30*time.Second) assertErr := services.AssertNoScopedCollectionMissed(assertCtx, db.Database) assertCancel() if assertErr != nil { log.Fatalf("scoped collection check failed: %v", assertErr) } gfCtx, gfCancel := context.WithTimeout(context.Background(), 2*time.Minute) gfErr := services.MigrateGrandfatherLicences(gfCtx, db.Database) gfCancel() if gfErr != nil { log.Fatalf("licence grandfather migration failed: %v", gfErr) } if err := services.EnsureAuthIndexes(); err != nil { log.Fatalf("failed to ensure auth indexes: %v", err) } if err := services.EnsureSecretIndexes(); err != nil { log.Printf("warning: failed to ensure secret indexes: %v", err) } if err := services.EnsureSettingsIndexes(); err != nil { log.Fatalf("failed to ensure settings indexes: %v", err) } if err := services.EnsureWorkflowIndexes(); err != nil { log.Printf("warning: failed to ensure workflow indexes: %v", err) } if instanceIDs, err := services.ListInstanceIDs(); err != nil { log.Printf("warning: failed to list instances for default step seeding: %v", err) } else { for _, instanceID := range instanceIDs { if created, updated, err := services.SeedDefaultSteps(instanceID); err != nil { log.Printf("warning: failed to seed default steps for instance %s: %v", instanceID, err) } else { log.Printf("default steps seeded for instance %s: %d created, %d updated", instanceID, created, updated) } } } services.StartLogSweeper() redisAddr := getEnv("REDIS_ADDR", "localhost:6379") if err := auth.InitRedis(redisAddr); err != nil { log.Fatalf("failed to connect to Redis: %v", err) } log.Println("connected to Redis") go func() { ticker := time.NewTicker(2 * time.Minute) defer ticker.Stop() for range ticker.C { if err := services.MarkOfflineServers(); err != nil { log.Printf("mark offline error: %v", err) } } }() go func() { if err := grpcserver.StartGRPC(9090); err != nil { log.Fatalf("gRPC server error: %v", err) } }() monitorsched.Start(context.Background()) r := gin.New() r.Use(gin.Recovery()) r.Use(gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/api/console/tunnel"}})) r.Use(corsMiddleware()) api.RegisterRoutes(r) log.Println("REST server listening on :8080") if err := r.Run(":8080"); err != nil { log.Fatalf("REST server error: %v", err) } } func corsMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }