package main import ( "fmt" "os" "strings" "git.solsynth.dev/goatworks/turbine/pkg/shared/registrar" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/proxy" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) var serviceDiscovery *registrar.ServiceDiscovery func init() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } func main() { log.Info().Msg("Starting Turbine Gateway...") viper.SetConfigName("settings") viper.AddConfigPath(".") viper.AddConfigPath("/etc/turbine/") viper.SetConfigType("toml") log.Info().Msg("Reading configuration...") if err := viper.ReadInConfig(); err != nil { log.Fatal().Err(err).Msg("Failed to read config file...") } log.Info().Msg("Configuration loaded.") // Setup service discovery etcdEndpoints := viper.GetStringSlice("etcd.endpoints") if len(etcdEndpoints) == 0 { log.Fatal().Msg("etcd.endpoints not configured in settings.toml") } if viper.GetBool("etcd.insecure") { log.Info().Msg("Using insecure transport for etcd") for i, ep := range etcdEndpoints { if !strings.HasPrefix(ep, "http://") && !strings.HasPrefix(ep, "https://") { etcdEndpoints[i] = "http://" + ep } } } log.Info().Strs("endpoints", etcdEndpoints).Msg("Connecting to etcd...") var err error serviceDiscovery, err = registrar.NewServiceDiscovery(etcdEndpoints) if err != nil { log.Fatal().Err(err).Msg("Failed to create service discovery client") } log.Info().Msg("Service discovery client created.") log.Info().Msg("Fetching initial service list...") if err := serviceDiscovery.Start(); err != nil { log.Fatal().Err(err).Msg("Failed to start service discovery") } log.Info().Msg("Service discovery started.") app := fiber.New(fiber.Config{ ServerHeader: "Turbine Gateway", BodyLimit: 2147483647, }) // Health check and status app.Get("/", func(c fiber.Ctx) error { return c.JSON(fiber.Map{ "status": "running", "services": serviceDiscovery.GetServiceRoutes(), }) }) // Handle route overrides first routeOverrides := viper.GetStringMapString("routes") for from, to := range routeOverrides { toParts := strings.SplitN(strings.TrimPrefix(to, "/"), "/", 2) if len(toParts) < 1 { log.Warn().Str("from", from).Str("to", to).Msg("Invalid route override config") continue } serviceName := toParts[0] var remainingPath string if len(toParts) > 1 { remainingPath = toParts[1] } log.Info().Str("from", from).Str("to", to).Msg("Applying route override") app.Use(from, func(c fiber.Ctx) error { instance, err := serviceDiscovery.GetNextInstance(serviceName) if err != nil { log.Info().Err(err).Str("service", serviceName).Msg("No service found") return fiber.ErrNotFound } targetURL := fmt.Sprintf("http://%s/%s", instance, remainingPath) originalPath := strings.TrimPrefix(c.Path(), from) if originalPath != "" { targetURL = fmt.Sprintf("%s%s", targetURL, originalPath) } if len(c.Request().URI().QueryString()) > 0 { targetURL = fmt.Sprintf("%s?%s", targetURL, string(c.Request().URI().QueryString())) } log.Info().Str("from", c.Path()).Str("to", targetURL).Msg("Forwarding with override") if err := proxy.Do(c, targetURL); err != nil { return err } c.Response().SetStatusCode(fiber.StatusOK) return nil }) } // Generic proxy handler app.Use("/*", func(c fiber.Ctx) error { path := c.Path() parts := strings.Split(strings.TrimPrefix(path, "/"), "/") if len(parts) < 1 || parts[0] == "" { // Let the health check handle this return c.Next() } serviceName := parts[0] remainingPath := strings.Join(parts[1:], "/") instance, err := serviceDiscovery.GetNextInstance(serviceName) if err != nil { log.Warn().Err(err).Str("service", serviceName).Msg("Failed to get service instance") return c.Status(fiber.StatusServiceUnavailable).SendString(err.Error()) } targetURL := fmt.Sprintf("http://%s/api/%s", instance, remainingPath) if len(c.Request().URI().QueryString()) > 0 { targetURL = fmt.Sprintf("%s?%s", targetURL, string(c.Request().URI().QueryString())) } log.Info().Str("from", path).Str("to", targetURL).Msg("Forwarding request") if err := proxy.Do(c, targetURL); err != nil { return err } c.Response().SetStatusCode(fiber.StatusOK) return nil }) listenAddr := viper.GetString("listen") if listenAddr == "" { listenAddr = ":8080" // default } log.Info().Msg("Gateway is listening on " + listenAddr) err = app.Listen(listenAddr, fiber.ListenConfig{ DisableStartupMessage: true, }) if err != nil { log.Fatal().Err(err).Msg("Failed to start server...") } }