Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 57 additions & 22 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -60,33 +61,60 @@ 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)

// 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
}
}

// find matched repos and check for worktree diffs
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
}

Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
41 changes: 37 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/utilitywarehouse/git-mirror/pkg/mirror"
)

Expand Down Expand Up @@ -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()
Expand All @@ -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"))}

Expand All @@ -93,22 +119,29 @@ 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)

<-stop

logger.Info("shutting down...")
if err := server.Shutdown(ctx); err != nil {
logger.Error("failed to shutdown http server", "err", err)
}
cancel()

select {
Expand Down