mirror of
https://github.com/cooperspencer/gickup.git
synced 2025-05-11 17:55:34 +02:00
309 lines
7 KiB
Go
309 lines
7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/cooperspencer/gickup/onedev"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kong"
|
|
"github.com/cooperspencer/gickup/bitbucket"
|
|
"github.com/cooperspencer/gickup/gitea"
|
|
"github.com/cooperspencer/gickup/github"
|
|
"github.com/cooperspencer/gickup/gitlab"
|
|
"github.com/cooperspencer/gickup/gogs"
|
|
"github.com/cooperspencer/gickup/local"
|
|
"github.com/cooperspencer/gickup/logger"
|
|
"github.com/cooperspencer/gickup/metrics/heartbeat"
|
|
"github.com/cooperspencer/gickup/metrics/prometheus"
|
|
"github.com/cooperspencer/gickup/types"
|
|
"github.com/cooperspencer/gickup/whatever"
|
|
"github.com/robfig/cron/v3"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var cli struct {
|
|
Configfile string `arg name:"conf" help:"Path to the configfile." default:"conf.yml"`
|
|
Version bool `flag name:"version" help:"Show version."`
|
|
Dry bool `flag name:"dryrun" help:"Make a dry-run."`
|
|
Quiet bool `flag name:"quiet" help:"Output only warnings, errors, and fatal messages to stderr log output"`
|
|
Silent bool `flag name:"silent" help:"Suppress all stderr log output"`
|
|
}
|
|
|
|
var version = "unknown"
|
|
|
|
func readConfigFile(configfile string) *types.Conf {
|
|
cfgdata, err := ioutil.ReadFile(configfile)
|
|
if err != nil {
|
|
log.Fatal().
|
|
Str("stage", "readconfig").
|
|
Str("file", configfile).
|
|
Msgf("Cannot open config file from %s", types.Red(configfile))
|
|
}
|
|
|
|
t := types.Conf{}
|
|
|
|
err = yaml.Unmarshal(cfgdata, &t)
|
|
|
|
if err != nil {
|
|
log.Fatal().
|
|
Str("stage", "readconfig").
|
|
Str("file", configfile).
|
|
Msg("Cannot map yml config file to interface, possible syntax error")
|
|
}
|
|
|
|
return &t
|
|
}
|
|
|
|
func getUserHome() (string, error) {
|
|
usr, err := user.Current()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return usr.HomeDir, nil
|
|
}
|
|
|
|
func substituteHomeForTildeInPath(path string) string {
|
|
if !strings.HasPrefix(path, "~") {
|
|
return path
|
|
}
|
|
|
|
if path == "~" {
|
|
userHome, err := getUserHome()
|
|
if err != nil {
|
|
log.Fatal().
|
|
Str("stage", "local ~ substitution").
|
|
Str("path", path).
|
|
Msg(err.Error())
|
|
} else {
|
|
return userHome
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(path, "~/") {
|
|
userHome, err := getUserHome()
|
|
if err != nil {
|
|
log.Fatal().
|
|
Str("stage", "local ~/ substitution").
|
|
Str("path", path).
|
|
Msg(err.Error())
|
|
} else {
|
|
return filepath.Join(userHome, path[2:])
|
|
}
|
|
}
|
|
// in whatever other strange case
|
|
return path
|
|
}
|
|
|
|
func backup(repos []types.Repo, conf *types.Conf) {
|
|
checkedpath := false
|
|
|
|
for _, r := range repos {
|
|
log.Info().
|
|
Str("stage", "backup").
|
|
Msgf("starting backup for %s", r.URL)
|
|
|
|
for i, d := range conf.Destination.Local {
|
|
if !checkedpath {
|
|
d.Path = substituteHomeForTildeInPath(d.Path)
|
|
|
|
path, err := filepath.Abs(d.Path)
|
|
if err != nil {
|
|
log.Fatal().
|
|
Str("stage", "locally").
|
|
Str("path", d.Path).
|
|
Msg(err.Error())
|
|
}
|
|
|
|
conf.Destination.Local[i].Path = path
|
|
checkedpath = true
|
|
}
|
|
|
|
local.Locally(r, d, cli.Dry)
|
|
prometheus.DestinationBackupsComplete.WithLabelValues("local").Inc()
|
|
}
|
|
|
|
for _, d := range conf.Destination.Gitea {
|
|
gitea.Backup(r, d, cli.Dry)
|
|
prometheus.DestinationBackupsComplete.WithLabelValues("gitea").Inc()
|
|
}
|
|
|
|
for _, d := range conf.Destination.Gogs {
|
|
gogs.Backup(r, d, cli.Dry)
|
|
prometheus.DestinationBackupsComplete.WithLabelValues("gogs").Inc()
|
|
}
|
|
|
|
for _, d := range conf.Destination.Gitlab {
|
|
gitlab.Backup(r, d, cli.Dry)
|
|
prometheus.DestinationBackupsComplete.WithLabelValues("gitlab").Inc()
|
|
}
|
|
|
|
prometheus.SourceBackupsComplete.WithLabelValues(r.Name).Inc()
|
|
}
|
|
}
|
|
|
|
func runBackup(conf *types.Conf) {
|
|
log.Info().Msg("Backup run starting")
|
|
|
|
startTime := time.Now()
|
|
|
|
prometheus.JobsStarted.Inc()
|
|
|
|
// Github
|
|
repos := github.Get(conf)
|
|
prometheus.CountReposDiscovered.WithLabelValues("github").Set(float64(len(repos)))
|
|
backup(repos, conf)
|
|
|
|
// Gitea
|
|
repos = gitea.Get(conf)
|
|
prometheus.CountReposDiscovered.WithLabelValues("gitea").Set(float64(len(repos)))
|
|
backup(repos, conf)
|
|
|
|
// Gogs
|
|
repos = gogs.Get(conf)
|
|
prometheus.CountReposDiscovered.WithLabelValues("gogs").Set(float64(len(repos)))
|
|
backup(repos, conf)
|
|
|
|
// Gitlab
|
|
repos = gitlab.Get(conf)
|
|
prometheus.CountReposDiscovered.WithLabelValues("gitlab").Set(float64(len(repos)))
|
|
backup(repos, conf)
|
|
|
|
repos = bitbucket.Get(conf)
|
|
prometheus.CountReposDiscovered.WithLabelValues("bitbucket").Set(float64(len(repos)))
|
|
backup(repos, conf)
|
|
|
|
repos = whatever.Get(conf)
|
|
prometheus.CountReposDiscovered.WithLabelValues("whatever").Set(float64(len(repos)))
|
|
backup(repos, conf)
|
|
|
|
repos = onedev.Get(conf)
|
|
prometheus.CountReposDiscovered.WithLabelValues("onedev").Set(float64(len(repos)))
|
|
backup(repos, conf)
|
|
|
|
endTime := time.Now()
|
|
duration := endTime.Sub(startTime)
|
|
|
|
prometheus.JobsComplete.Inc()
|
|
prometheus.JobDuration.Observe(duration.Seconds())
|
|
|
|
if len(conf.Metrics.Heartbeat.URLs) > 0 {
|
|
heartbeat.Send(conf.Metrics.Heartbeat)
|
|
}
|
|
|
|
log.Info().
|
|
Str("duration", duration.String()).
|
|
Msg("Backup run complete")
|
|
|
|
if conf.HasValidCronSpec() {
|
|
logNextRun(conf)
|
|
}
|
|
}
|
|
|
|
func playsForever() {
|
|
wait := make(chan struct{})
|
|
|
|
for {
|
|
<-wait
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
timeformat := "2006-01-02T15:04:05Z07:00"
|
|
|
|
if len(os.Getenv("GICKUP_TIME_FORMAT")) > 0 {
|
|
timeformat = os.Getenv("GICKUP_TIME_FORMAT")
|
|
}
|
|
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{
|
|
Out: os.Stderr,
|
|
TimeFormat: timeformat,
|
|
})
|
|
|
|
kong.Parse(&cli, kong.Name("gickup"),
|
|
kong.Description("a tool to backup all your favorite repos"))
|
|
|
|
if cli.Version {
|
|
fmt.Println(version)
|
|
|
|
return
|
|
}
|
|
|
|
if cli.Quiet {
|
|
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
|
}
|
|
|
|
if cli.Silent {
|
|
zerolog.SetGlobalLevel(zerolog.Disabled)
|
|
}
|
|
|
|
if cli.Dry {
|
|
log.Info().
|
|
Str("dry", "true").
|
|
Msgf("this is a %s", types.Blue("dry run"))
|
|
}
|
|
|
|
log.Info().Str("file", cli.Configfile).
|
|
Msgf("Reading %s", types.Green(cli.Configfile))
|
|
|
|
conf := readConfigFile(cli.Configfile)
|
|
if conf.Log.Timeformat == "" {
|
|
conf.Log.Timeformat = timeformat
|
|
}
|
|
|
|
log.Logger = logger.CreateLogger(conf.Log)
|
|
|
|
// one pair per source-destination
|
|
pairs := conf.Source.Count() * conf.Destination.Count()
|
|
log.Info().
|
|
Int("sources", conf.Source.Count()).
|
|
Int("destinations", conf.Destination.Count()).
|
|
Int("pairs", pairs).
|
|
Msg("Configuration loaded")
|
|
|
|
if conf.HasValidCronSpec() {
|
|
c := cron.New()
|
|
|
|
logNextRun(conf)
|
|
|
|
_, err := c.AddFunc(conf.Cron, func() {
|
|
runBackup(conf)
|
|
})
|
|
if err != nil {
|
|
log.Fatal().
|
|
Int("sources", conf.Source.Count()).
|
|
Int("destinations", conf.Destination.Count()).
|
|
Int("pairs", pairs).
|
|
Msg(err.Error())
|
|
}
|
|
|
|
c.Start()
|
|
|
|
if conf.HasAllPrometheusConf() {
|
|
prometheus.CountSourcesConfigured.Add(float64(conf.Source.Count()))
|
|
prometheus.CountDestinationsConfigured.Add(float64(conf.Destination.Count()))
|
|
prometheus.Serve(conf.Metrics.Prometheus)
|
|
} else {
|
|
playsForever()
|
|
}
|
|
} else {
|
|
runBackup(conf)
|
|
}
|
|
}
|
|
|
|
func logNextRun(conf *types.Conf) {
|
|
nextRun, err := conf.GetNextRun()
|
|
if err == nil {
|
|
log.Info().
|
|
Str("next", nextRun.String()).
|
|
Str("cron", conf.Cron).
|
|
Msg("Next cron run")
|
|
}
|
|
}
|