Files
creator-hub/cmd/control-plane/main.go
T

187 lines
5.4 KiB
Go

package main
import (
"context"
"errors"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"git.ipao.vip/rogee/creator-hub/internal/phasea"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/adaptor"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
type config struct {
listenAddr string
gateway *url.URL
webDir string
databaseURL string
logLevel logrus.Level
}
func main() {
logrus.SetFormatter(&logrus.JSONFormatter{})
if err := execute(newCommand()); err != nil {
logrus.WithField("service", "control-plane").WithError(err).Error("service stopped")
os.Exit(1)
}
}
func execute(command *cobra.Command) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
return command.ExecuteContext(ctx)
}
func newCommand() *cobra.Command {
command := &cobra.Command{
Use: "control-plane",
Short: "Run the CreatorHub control plane",
Args: cobra.NoArgs,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(command *cobra.Command, _ []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
logrus.SetLevel(cfg.logLevel)
store, err := phasea.Open(command.Context(), cfg.databaseURL)
if err != nil {
return err
}
defer store.Close()
logStartup(cfg)
return newHandlerWithPhaseA(cfg.gateway, cfg.webDir, store).Listen(cfg.listenAddr, fiber.ListenConfig{
GracefulContext: command.Context(),
DisableStartupMessage: true,
})
},
}
return command
}
func logStartup(cfg config) {
logrus.WithFields(logrus.Fields{
"service": "control-plane",
"listen_addr": cfg.listenAddr,
"docker_gateway": cfg.gateway.Scheme + "://" + cfg.gateway.Host,
}).Info("service starting")
}
func loadConfig() (config, error) {
v := viper.New()
v.SetDefault("listen_addr", ":8080")
v.SetDefault("docker_gateway_url", "http://127.0.0.1:8081")
v.SetDefault("web_dir", "web/dist")
v.SetDefault("database_url", "postgres://creatorhub@127.0.0.1:5432/creatorhub?sslmode=disable")
v.SetDefault("log_level", "info")
_ = v.BindEnv("listen_addr", "LISTEN_ADDR")
_ = v.BindEnv("docker_gateway_url", "DOCKER_GATEWAY_URL")
_ = v.BindEnv("web_dir", "WEB_DIR")
_ = v.BindEnv("database_url", "DATABASE_URL")
_ = v.BindEnv("log_level", "LOG_LEVEL")
gateway, err := url.Parse(v.GetString("docker_gateway_url"))
if err != nil || gateway.Host == "" || (gateway.Scheme != "http" && gateway.Scheme != "https") {
return config{}, errors.New("DOCKER_GATEWAY_URL must be an http(s) URL with a host")
}
level, err := logrus.ParseLevel(v.GetString("log_level"))
if err != nil {
return config{}, errors.New("LOG_LEVEL must be panic, fatal, error, warn, info, debug, or trace")
}
cfg := config{
listenAddr: strings.TrimSpace(v.GetString("listen_addr")),
gateway: gateway,
webDir: strings.TrimSpace(v.GetString("web_dir")),
databaseURL: strings.TrimSpace(v.GetString("database_url")),
logLevel: level,
}
if cfg.listenAddr == "" {
return config{}, errors.New("LISTEN_ADDR must not be empty")
}
if err := validateListenAddr(cfg.listenAddr); err != nil {
return config{}, err
}
if cfg.webDir == "" {
return config{}, errors.New("WEB_DIR must not be empty")
}
databaseURL, err := url.Parse(cfg.databaseURL)
if err != nil || databaseURL.Host == "" || databaseURL.Path == "" || databaseURL.Path == "/" ||
(databaseURL.Scheme != "postgres" && databaseURL.Scheme != "postgresql") {
return config{}, errors.New("DATABASE_URL must be a postgres URL with a host")
}
return cfg, nil
}
func validateListenAddr(addr string) error {
_, port, err := net.SplitHostPort(addr)
if err != nil {
return errors.New("LISTEN_ADDR must be a host:port address")
}
number, err := strconv.Atoi(port)
if err != nil || number < 1 || number > 65535 {
return errors.New("LISTEN_ADDR port must be 1..65535")
}
return nil
}
func newHandler(gateway *url.URL, webDirectory string) *fiber.App {
return newHandlerWithPhaseA(gateway, webDirectory, nil)
}
func newHandlerWithPhaseA(gateway *url.URL, webDirectory string, store *phasea.Store) *fiber.App {
proxy := httputil.NewSingleHostReverseProxy(gateway)
originalDirector := proxy.Director
proxy.Director = func(request *http.Request) {
originalDirector(request)
request.URL.Path = "/v1/browsers" + strings.TrimPrefix(request.URL.Path, "/api/browsers")
request.Host = gateway.Host
}
proxyHandler := adaptor.HTTPHandler(proxy)
app := fiber.New(fiber.Config{
AppName: "CreatorHub control plane",
BodyLimit: 1 << 20,
ReadTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
})
app.Get("/healthz", func(c fiber.Ctx) error {
c.Status(fiber.StatusNoContent)
return nil
})
app.All("/api/browsers", proxyHandler)
app.All("/api/browsers/*", proxyHandler)
if store != nil {
registerPhaseA(app, store)
}
app.Get("/*", spaHandler(webDirectory))
return app
}
func spaHandler(directory string) fiber.Handler {
return func(c fiber.Ctx) error {
path, err := url.PathUnescape(strings.TrimPrefix(c.Path(), "/"))
path = filepath.Clean(path)
if err == nil && filepath.IsLocal(path) {
name := filepath.Join(directory, path)
if info, err := os.Stat(name); err == nil && !info.IsDir() {
return c.SendFile(name)
}
}
return c.SendFile(filepath.Join(directory, "index.html"))
}
}