package main import ( "context" "flag" "log" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "provisioning/internal/api" "provisioning/internal/config" "provisioning/internal/db" "provisioning/internal/events" "provisioning/internal/httpserver" "provisioning/internal/infra" "provisioning/internal/orchestrator" "provisioning/internal/pxe" "provisioning/internal/store" ) func main() { cfgPath := flag.String("config", "provisioning.yaml", "path to config file") flag.Parse() cfg, err := config.Load(*cfgPath) if err != nil { log.Fatalf("config: %v", err) } serverTypes, err := config.LoadServerTypes(cfg.ServerTypePath) if err != nil { log.Fatalf("server types: %v", err) } if err := os.MkdirAll(filepath.Dir(cfg.Database.Path), 0o755); err != nil { log.Fatalf("create db dir: %v", err) } database, err := db.Open(cfg.Database.Path) if err != nil { log.Fatalf("database: %v", err) } defer database.Close() hosts := &store.Hosts{DB: database} ops := &store.Operations{DB: database} locks := &store.Locks{DB: database, TTLMinutes: cfg.Locks.TTLMinutes} images := &store.Images{DB: database} hub := events.NewHub() runner := &orchestrator.Runner{ Hosts: hosts, Ops: ops, Locks: locks, Hub: hub, } pxeSupervisor := pxe.NewSupervisor(pxe.SupervisorConfig{ Enabled: cfg.PXE.Enabled, Interface: cfg.PXE.Interface, Subnet: cfg.PXE.Subnet, RuntimeDir: cfg.PXE.RuntimeDir, TFTPRoot: cfg.PXE.TFTPRoot, DnsmasqBin: cfg.PXE.DnsmasqBin, PublicURL: cfg.Server.PublicURL, }) var infraClient *infra.Client if cfg.Infrastructure.BaseURL != "" { infraClient = infra.NewClient(cfg.Infrastructure.BaseURL, time.Duration(cfg.Infrastructure.TimeoutSec)*time.Second) } clusterJoiner := &orchestrator.ClusterJoiner{ ExistingNode: cfg.Proxmox.ExistingNode, ClusterName: cfg.Proxmox.ClusterName, JoinFingerprint: cfg.Proxmox.JoinFingerprint, } hostOrch := &orchestrator.HostOrchestrator{ Runner: runner, Hosts: hosts, Ops: ops, Locks: locks, Cluster: clusterJoiner, InfraClient: infraClient, Config: cfg, ServerTypes: serverTypes, } hostAPI := &api.HostAPI{ Hosts: hosts, Ops: ops, Locks: locks, Images: images, Runner: runner, Orchestrator: hostOrch, PXE: pxeSupervisor, Config: cfg, ServerTypes: serverTypes, } bootAPI := &api.BootAPI{ Hosts: hosts, Images: images, Runner: runner, Orchestrator: hostOrch, Config: cfg, ServerTypes: serverTypes, } ui := &api.UI{ Hosts: hosts, Ops: ops, Locks: locks, Images: images, Runner: runner, Orchestrator: hostOrch, Hub: hub, PXE: pxeSupervisor, Config: cfg, ServerTypes: serverTypes, } router := httpserver.NewRouter(httpserver.Deps{ HostAPI: hostAPI, BootAPI: bootAPI, UI: ui, Hub: hub, }) srv := &http.Server{ Addr: cfg.Server.Bind, Handler: router, } // Start PXE allHosts, _ := hosts.List(context.Background()) if err := pxeSupervisor.Start(context.Background(), allHosts); err != nil { log.Printf("pxe: failed to start: %v", err) } // Watch server types for hot reload stop := make(chan struct{}) serverTypes.Watch(stop) // Start HTTP server go func() { log.Printf("listening on %s", cfg.Server.Bind) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("http: %v", err) } }() // Graceful shutdown shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) <-shutdown log.Printf("shutting down") close(stop) _ = pxeSupervisor.Shutdown() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("http shutdown: %v", err) } _ = hub.Shutdown(ctx) }