package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/config" "github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/hetzner" "github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/ipresolve" "github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/scheduler" "github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/secure" "github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/store" "github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/web" ) func main() { if len(os.Args) > 1 && os.Args[1] == "healthcheck" { os.Exit(healthcheck()) } logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) if err := run(logger); err != nil { logger.Error("application stopped", "error", err) os.Exit(1) } } func run(logger *slog.Logger) error { cfg, err := config.Load() if err != nil { return err } cipher, err := secure.LoadCipher(cfg.MasterKeyFile) if err != nil { return err } ctx := context.Background() database, err := store.Open(ctx, cfg.DatabasePath) if err != nil { return err } defer database.Close() if err := database.Prune(ctx); err != nil { logger.Warn("prune old local data", "error", err) } provider := hetzner.New(cfg.HCloudBaseURL) updater := scheduler.New(database, provider, ipresolve.New(), cipher, logger) settings, err := database.GetSettings(ctx) if err != nil { return err } if !settings.IPv6Enabled { deleted, cleanupErr := updater.DisableIPv6(ctx) if cleanupErr != nil { logger.Warn("enforce disabled IPv6", "error", cleanupErr) } else if deleted > 0 { logger.Info("removed AAAA records for disabled IPv6", "count", deleted) } } serverUI, err := web.New(cfg, database, cipher, updater, logger) if err != nil { return err } appCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() updater.Start(appCtx) httpServer := &http.Server{ Addr: cfg.Address, Handler: serverUI.Handler(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } serverErrors := make(chan error, 1) go func() { logger.Info("starting server", "address", cfg.Address, "mode", cfg.Mode) serverErrors <- httpServer.ListenAndServe() }() select { case err := <-serverErrors: if !errors.Is(err, http.ErrServerClosed) { return err } return nil case <-appCtx.Done(): logger.Info("shutdown requested") shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer cancel() return httpServer.Shutdown(shutdownCtx) } } func healthcheck() int { url := strings.TrimSpace(os.Getenv("APP_HEALTHCHECK_URL")) if url == "" { url = "http://127.0.0.1:8080/healthz" } client := &http.Client{Timeout: 3 * time.Second} response, err := client.Get(url) if err != nil { return 1 } defer response.Body.Close() if response.StatusCode != http.StatusOK { return 1 } return 0 }