Files
silo/cmd/silod/main.go
2026-01-24 15:03:17 -06:00

127 lines
3.4 KiB
Go

// Command silod is the Silo HTTP API server.
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/kindredsystems/silo/internal/api"
"github.com/kindredsystems/silo/internal/config"
"github.com/kindredsystems/silo/internal/db"
"github.com/kindredsystems/silo/internal/schema"
"github.com/kindredsystems/silo/internal/storage"
"github.com/rs/zerolog"
)
func main() {
// Parse flags
configPath := flag.String("config", "config.yaml", "Path to configuration file")
flag.Parse()
// Setup logger
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
// Load configuration
cfg, err := config.Load(*configPath)
if err != nil {
logger.Fatal().Err(err).Msg("failed to load configuration")
}
logger.Info().
Str("host", cfg.Server.Host).
Int("port", cfg.Server.Port).
Str("database", cfg.Database.Host).
Str("storage", cfg.Storage.Endpoint).
Msg("starting silo server")
// Connect to database
ctx := context.Background()
database, err := db.Connect(ctx, db.Config{
Host: cfg.Database.Host,
Port: cfg.Database.Port,
Name: cfg.Database.Name,
User: cfg.Database.User,
Password: cfg.Database.Password,
SSLMode: cfg.Database.SSLMode,
MaxConnections: cfg.Database.MaxConnections,
})
if err != nil {
logger.Fatal().Err(err).Msg("failed to connect to database")
}
defer database.Close()
logger.Info().Msg("connected to database")
// Connect to storage (optional - may be externally managed)
var store *storage.Storage
if cfg.Storage.Endpoint != "" {
store, err = storage.Connect(ctx, storage.Config{
Endpoint: cfg.Storage.Endpoint,
AccessKey: cfg.Storage.AccessKey,
SecretKey: cfg.Storage.SecretKey,
Bucket: cfg.Storage.Bucket,
UseSSL: cfg.Storage.UseSSL,
Region: cfg.Storage.Region,
})
if err != nil {
logger.Warn().Err(err).Msg("failed to connect to storage - file operations disabled")
store = nil
} else {
logger.Info().Msg("connected to storage")
}
} else {
logger.Info().Msg("storage not configured - file operations disabled")
}
// Load schemas
schemas, err := schema.LoadAll(cfg.Schemas.Directory)
if err != nil {
logger.Fatal().Err(err).Str("directory", cfg.Schemas.Directory).Msg("failed to load schemas")
}
logger.Info().Int("count", len(schemas)).Msg("loaded schemas")
// Create API server
server := api.NewServer(logger, database, schemas, cfg.Schemas.Directory, store)
router := api.NewRouter(server, logger)
// Create HTTP server
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
httpServer := &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in goroutine
go func() {
logger.Info().Str("addr", addr).Msg("listening")
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal().Err(err).Msg("server error")
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info().Msg("shutting down server")
// Graceful shutdown with timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
logger.Fatal().Err(err).Msg("server forced to shutdown")
}
logger.Info().Msg("server stopped")
}