package main import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/docker/docker/client" "github.com/prometheus/client_golang/prometheus" "github.com/shcizo/package-updater/internal/api" "github.com/shcizo/package-updater/internal/config" "github.com/shcizo/package-updater/internal/discovery" "github.com/shcizo/package-updater/internal/logging" "github.com/shcizo/package-updater/internal/metrics" "github.com/shcizo/package-updater/internal/updater" ) // Build info is injected at link time via -ldflags="-X main.version=..." var ( version = "dev" commit = "unknown" buildTime = "unknown" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "fatal:", err) os.Exit(1) } } func run() error { cfg, err := config.Load() if err != nil { return err } logger := logging.New(cfg.LogLevel) logger.Info("starting", "version", version, "commit", commit, "port", cfg.Port, "stacks_root", cfg.StacksRoot, "opt_in_label", cfg.OptInLabel, ) dockerCli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return fmt.Errorf("docker client: %w", err) } defer dockerCli.Close() disc := discovery.New(dockerCli, cfg.StacksRoot, cfg.OptInLabel) exec := updater.NewComposeExecutor() reg := prometheus.NewRegistry() m := metrics.New(reg) m.BuildInfo.WithLabelValues(version, commit).Set(1) queue := updater.NewQueue(exec, m) queue.Start(context.Background()) defer queue.Stop() handlers := api.NewHandlers(disc, &submitterAdapter{queue: queue, timeout: cfg.UpdateTimeout}, dockerCli, version, commit, buildTime, m) mux := http.NewServeMux() mux.HandleFunc("POST /update", handlers.Update) mux.HandleFunc("GET /healthz", handlers.Healthz) mux.HandleFunc("GET /version", handlers.Version) mux.Handle("GET /metrics", metrics.Handler(reg)) authed := api.Auth(cfg.APIKey) handler := api.RequestID(api.RequestLogger(logger, m)(routeAuth(mux, authed))) srv := &http.Server{ Addr: ":" + cfg.Port, Handler: handler, ReadHeaderTimeout: 10 * time.Second, } errCh := make(chan error, 1) go func() { logger.Info("listening", "addr", srv.Addr) errCh <- srv.ListenAndServe() }() sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) select { case s := <-sig: logger.Info("shutdown_signal", "signal", s.String()) case err := <-errCh: if err != nil && err != http.ErrServerClosed { return err } } shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = srv.Shutdown(shutdownCtx) return nil } // routeAuth applies the bearer-token middleware to /update only. // /healthz, /metrics, /version are unauthenticated by design (internal // network only; healthcheck and Prometheus scraper need to reach them // without secrets). func routeAuth(next *http.ServeMux, mw func(http.Handler) http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/update" { mw(next).ServeHTTP(w, r) return } next.ServeHTTP(w, r) }) } // submitterAdapter wraps Queue.Submit to inject the per-request timeout // from config, satisfying the api.Submitter interface. type submitterAdapter struct { queue *updater.Queue timeout time.Duration } func (s *submitterAdapter) Submit(ctx context.Context, jobs []discovery.Job) []updater.Result { ctx, cancel := context.WithTimeout(ctx, s.timeout) defer cancel() return s.queue.Submit(ctx, jobs) }