diff --git a/config.go b/config.go index 299bce5..a5f9c11 100644 --- a/config.go +++ b/config.go @@ -7,6 +7,7 @@ import ( "path" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/utilitywarehouse/git-mirror/pkg/giturl" "github.com/utilitywarehouse/git-mirror/pkg/mirror" "gopkg.in/yaml.v3" @@ -20,31 +21,31 @@ const ( defaultSSHKnownHostsPath = "/etc/git-secret/known_hosts" ) -var defaultRoot = path.Join(os.TempDir(), "git-mirror") +var ( + defaultRoot = path.Join(os.TempDir(), "git-mirror") + + configSuccess = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "git_mirror_config_last_reload_successful", + Help: "Whether the last configuration reload attempt was successful.", + }) + configSuccessTime = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "git_mirror_config_last_reload_success_timestamp_seconds", + Help: "Timestamp of the last successful configuration reload.", + }) +) // WatchConfig polls the config file every interval and reloads if modified -func WatchConfig(ctx context.Context, path string, watchConfig bool, interval time.Duration, onChange func(*mirror.RepoPoolConfig)) { +func WatchConfig(ctx context.Context, path string, watchConfig bool, interval time.Duration, onChange func(*mirror.RepoPoolConfig) bool) { var lastModTime time.Time + var success bool for { - fileInfo, err := os.Stat(path) - if err != nil { - logger.Error("Error checking config file", "err", err) - time.Sleep(interval) // retry after given interval - continue - } - - modTime := fileInfo.ModTime() - if modTime.After(lastModTime) { - logger.Info("reloading config file...") - lastModTime = modTime - - newConfig, err := parseConfigFile(path) - if err != nil { - logger.Error("failed to reload config", "err", err) - } else { - onChange(newConfig) - } + lastModTime, success = loadConfig(path, lastModTime, onChange) + if success { + configSuccess.Set(1) + configSuccessTime.SetToCurrentTime() + } else { + configSuccess.Set(0) } if !watchConfig { @@ -60,7 +61,30 @@ func WatchConfig(ctx context.Context, path string, watchConfig bool, interval ti } } -func ensureConfig(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) { +func loadConfig(path string, lastModTime time.Time, onChange func(*mirror.RepoPoolConfig) bool) (time.Time, bool) { + fileInfo, err := os.Stat(path) + if err != nil { + logger.Error("Error checking config file", "err", err) + return lastModTime, false + } + + modTime := fileInfo.ModTime() + if modTime.Equal(lastModTime) { + return lastModTime, true + } + + logger.Info("reloading config file...") + + newConfig, err := parseConfigFile(path) + if err != nil { + logger.Error("failed to reload config", "err", err) + return lastModTime, false + } + return modTime, onChange(newConfig) +} + +func ensureConfig(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) bool { + success := true // add default values applyGitDefaults(newConfig) @@ -68,18 +92,20 @@ func ensureConfig(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) { // validate and apply defaults to new config before compare if err := newConfig.ValidateAndApplyDefaults(); err != nil { logger.Error("failed to validate new config", "err", err) - return + return false } newRepos, removedRepos := diffRepositories(repoPool, newConfig) for _, repo := range removedRepos { if err := repoPool.RemoveRepository(repo); err != nil { logger.Error("failed to remove repository", "remote", repo, "err", err) + success = false } } for _, repo := range newRepos { if err := repoPool.AddRepository(repo); err != nil { logger.Error("failed to add new repository", "remote", repo.Remote, "err", err) + success = false } } @@ -87,6 +113,8 @@ func ensureConfig(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) { for _, newRepoConf := range newConfig.Repositories { repo, err := repoPool.Repository(newRepoConf.Remote) if err != nil { + logger.Error("unable to check worktree changes", "remote", newRepoConf.Remote, "err", err) + success = false continue } @@ -96,14 +124,21 @@ func ensureConfig(repoPool *mirror.RepoPool, newConfig *mirror.RepoPoolConfig) { for _, wt := range removedWTs { if err := repoPool.RemoveWorktreeLink(newRepoConf.Remote, wt); err != nil { logger.Error("failed to remove worktree", "remote", newRepoConf.Remote, "link", wt, "err", err) + success = false } } for _, wt := range newWTs { if err := repoPool.AddWorktreeLink(newRepoConf.Remote, wt); err != nil { logger.Error("failed to add worktree", "remote", newRepoConf.Remote, "link", wt.Link, "err", err) + success = false } } } + + // start mirror Loop on newly added repos + repoPool.StartLoop() + + return success } func applyGitDefaults(mirrorConf *mirror.RepoPoolConfig) { diff --git a/go.mod b/go.mod index 0bb2844..b07f017 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect diff --git a/go.sum b/go.sum index 1acef4d..3cdac05 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= diff --git a/main.go b/main.go index 89fd0d3..8221bca 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,12 @@ package main import ( "context" + "errors" "flag" "fmt" "log/slog" + "net/http" + "net/http/pprof" "os" "os/signal" "strconv" @@ -12,6 +15,8 @@ import ( "syscall" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/utilitywarehouse/git-mirror/pkg/mirror" ) @@ -74,6 +79,7 @@ func main() { flagLogLevel := flag.String("log-level", envString("LOG_LEVEL", "info"), "Log level") flagConfig := flag.String("config", envString("GIT_MIRROR_CONFIG", "/etc/git-mirror/config.yaml"), "Absolute path to the config file") flagWatchConfig := flag.Bool("watch-config", envBool("GIT_MIRROR_WATCH_CONFIG", true), "watch config for changes and reload when changes encountered") + flagHttpBind := flag.String("http-bind-address", envString("GIT_MIRROR_HTTP_BIND", ":8098"), "The address the web server binds to") flag.Usage = usage flag.Parse() @@ -83,6 +89,26 @@ func main() { loggerLevel.Set(v) } + mirror.EnableMetrics("", prometheus.NewRegistry()) + prometheus.MustRegister(configSuccess, configSuccessTime) + + server := &http.Server{ + Addr: *flagHttpBind, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 5 * time.Second, + ReadHeaderTimeout: 1 * time.Second, + } + + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + server.Handler = mux + // path to resolve git gitENV := []string{fmt.Sprintf("PATH=%s", os.Getenv("PATH"))} @@ -93,15 +119,19 @@ func main() { os.Exit(1) } - onConfigChange := func(config *mirror.RepoPoolConfig) { - ensureConfig(repoPool, config) - // start mirror Loop on newly added repos - repoPool.StartLoop() + onConfigChange := func(config *mirror.RepoPoolConfig) bool { + return ensureConfig(repoPool, config) } // Start watching the config file go WatchConfig(ctx, *flagConfig, *flagWatchConfig, 10*time.Second, onConfigChange) + go func() { + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error("HTTP server terminated", "err", err) + } + }() + //listenForShutdown stop := make(chan os.Signal, 2) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) @@ -109,6 +139,9 @@ func main() { <-stop logger.Info("shutting down...") + if err := server.Shutdown(ctx); err != nil { + logger.Error("failed to shutdown http server", "err", err) + } cancel() select {