Federated via graft
3.9 kB
164 lines
1// Command sync runs the Forgejo <-> Radicle sync daemon: on an interval, it
2// mirrors git content, issues and patches for every repo pair listed in the
3// config file, then sleeps until the next pass.
4package main
5
6import (
7 "flag"
8 "log/slog"
9 "net/http"
10 "net/url"
11 "os"
12 "strings"
13 "time"
14
15 "graft/internal/activitypub"
16 "graft/internal/config"
17 "graft/internal/state"
18 "graft/internal/status"
19 "graft/internal/sync"
20)
21
22func main() {
23 configPath := flag.String("config", "/etc/graft/config.yaml", "path to config.yaml")
24 once := flag.Bool("once", false, "run a single pass and exit, instead of looping")
25 listen := flag.String("listen", ":8090", "address to serve /healthz and / (status) on; empty disables it")
26 flag.Parse()
27
28 log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
29
30 cfg, err := config.Load(*configPath)
31 if err != nil {
32 log.Error("load config", "err", err)
33 os.Exit(1)
34 }
35
36 st, err := state.Open(cfg.StateDB)
37 if err != nil {
38 log.Error("open state db", "err", err)
39 os.Exit(1)
40 }
41 defer st.Close()
42
43 stateDir := cfg.StateDB
44 if idx := lastSlash(stateDir); idx >= 0 {
45 stateDir = stateDir[:idx]
46 }
47
48 syncers := make([]*sync.RepoSyncer, 0, len(cfg.Repos))
49 for _, pair := range cfg.Repos {
50 rs, err := sync.New(pair, st, sync.WorkDirFor(stateDir, pair.Name))
51 if err != nil {
52 log.Error("set up repo pair", "pair", pair.Name, "err", err)
53 os.Exit(1)
54 }
55 syncers = append(syncers, rs)
56 }
57
58 topology := buildTopology(cfg)
59
60 tracker := status.NewTracker(st, cfg.SourceURL)
61 tracker.SetTopology(topology)
62 tracker.SetPublicHost(cfg.PublicHost)
63
64 var apHandler *activitypub.Handler
65 if cfg.PublicHost != "" {
66 apHandler = activitypub.NewHandler(st, cfg.PublicHost, log,
67 func(series string) string {
68 if refs := topology[series]; len(refs) > 0 {
69 return refs[0].URL
70 }
71 return ""
72 },
73 func(series string) bool {
74 _, ok := topology[series]
75 return ok
76 },
77 )
78 }
79
80 runAll := func() {
81 for _, rs := range syncers {
82 gitErr, issuesErr, patchErr := rs.Run(log)
83 tracker.Record(rs.Name(), rs.Series(), gitErr, issuesErr, patchErr)
84 }
85 if apHandler != nil {
86 apHandler.DeliverNewActivity()
87 }
88 }
89
90 if *listen != "" {
91 mux := http.NewServeMux()
92 if apHandler != nil {
93 apHandler.Register(mux)
94 }
95 mux.Handle("/", tracker.Handler())
96
97 go func() {
98 if err := http.ListenAndServe(*listen, mux); err != nil {
99 log.Error("status server stopped", "err", err)
100 }
101 }()
102 }
103
104 runAll()
105 if *once {
106 return
107 }
108
109 ticker := time.NewTicker(cfg.SyncInterval)
110 defer ticker.Stop()
111 for range ticker.C {
112 runAll()
113 }
114}
115
116// buildTopology declares every mirror target configured for each series
117// (its Forgejo side and its Radicle side), so the dashboard can show a
118// side that's never produced an event yet — not just sides the activity
119// log happens to mention.
120func buildTopology(cfg *config.Config) map[string][]status.ServerRef {
121 topology := map[string][]status.ServerRef{}
122 seen := map[string]bool{}
123 add := func(series, host, repoURL string) {
124 if host == "" {
125 return
126 }
127 key := series + "|" + host
128 if seen[key] {
129 return
130 }
131 seen[key] = true
132 topology[series] = append(topology[series], status.ServerRef{Label: host, URL: repoURL})
133 }
134
135 for _, pair := range cfg.Repos {
136 series := pair.Series
137 if series == "" {
138 series = pair.Name
139 }
140
141 fHost := ""
142 if u, err := url.Parse(pair.Forgejo.BaseURL); err == nil {
143 fHost = u.Host
144 }
145 fURL := strings.TrimRight(pair.Forgejo.BaseURL, "/") + "/" + pair.Forgejo.Owner + "/" + pair.Forgejo.Repo
146 add(series, fHost, fURL)
147
148 rHost := ""
149 if u, err := url.Parse(pair.Radicle.HTTPBaseURL); err == nil {
150 rHost = u.Host
151 }
152 add(series, rHost, sync.RadicleExplorerLink(pair.Radicle))
153 }
154 return topology
155}
156
157func lastSlash(s string) int {
158 for i := len(s) - 1; i >= 0; i-- {
159 if s[i] == '/' {
160 return i
161 }
162 }
163 return -1
164}