1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
package main
import (
"bytes"
_ "embed"
"errors"
"flag"
"fmt"
"log/slog"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"go.jolheiser.com/cuesonnet"
)
//go:embed schema.cue
var schema cuesonnet.Schema
func maine() error {
fs := flag.NewFlagSet("horcrux", flag.ExitOnError)
jsonFlag := fs.Bool("json", false, "Print logs in JSON format")
debugFlag := fs.Bool("debug", false, "Debug logging")
configFlag := fs.String("config", ".horcrux.jsonnet", "Path to config file")
if err := fs.Parse(os.Args[1:]); err != nil {
return err
}
level := slog.LevelInfo
if *debugFlag {
level = slog.LevelDebug
}
opts := &slog.HandlerOptions{
AddSource: true,
Level: level,
}
var handler slog.Handler = slog.NewTextHandler(os.Stderr, opts)
if *jsonFlag {
handler = slog.NewJSONHandler(os.Stderr, opts)
}
slog.SetDefault(slog.New(handler))
cfg, err := os.ReadFile(*configFlag)
if err != nil {
return fmt.Errorf("could not read config file: %w", err)
}
var config Config
if err := schema.Decode(bytes.NewReader(cfg), &config); err != nil {
return fmt.Errorf("could not decode config file: %w", err)
}
if err := os.MkdirAll(config.Storage, os.ModePerm); err != nil {
return fmt.Errorf("could not create storage repo at %q: %w", config.Storage, err)
}
git := sshGit(config.Key)
ticker := time.NewTicker(time.Duration(config.Interval))
go func() {
for {
slog.Debug("running sync...")
for _, r := range config.Repos {
go func(r RepoConfig) {
// Check if we need to clone first
repoPath := filepath.Join(config.Storage, r.Name)
_, err := os.Stat(repoPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
if err := git(config.Storage, "clone", "--mirror", r.Source, r.Name); err != nil {
slog.Error("could not clone repo", slog.String("repo", r.Source), slog.Any("err", err))
}
} else {
slog.Error("could not stat repo path", slog.Any("err", err))
}
}
// Update from remote
if err := git(repoPath, "remote", "update", "--prune"); err != nil {
slog.Error("could not update repo", slog.String("repo", r.Source), slog.Any("err", err))
}
// Push
for _, dest := range r.Dest {
slog.Debug("syncing repo", slog.String("repo", r.Source), slog.String("dest", dest))
if err := git(repoPath, "push", "--mirror", "--force", dest); err != nil {
slog.Error("could not push repo", slog.String("repo", r.Source), slog.String("dest", dest), slog.Any("err", err))
}
}
}(r)
}
<-ticker.C
}
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
<-ch
return nil
}
func sshGit(key string) func(string, ...string) error {
return func(dir string, args ...string) error {
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(), fmt.Sprintf(`GIT_SSH_COMMAND=ssh -o "StrictHostKeyChecking accept-new" -i %s`, key))
out, err := cmd.Output()
slog.Debug("git command", slog.String("args", strings.Join(args, " ")), slog.String("output", string(out)))
return err
}
}
func main() {
if err := maine(); err != nil {
slog.Error("error running horcrux", slog.Any("err", err))
}
}
|