Federated via graft
3.8 kB
126 lines
1// Package config loads and validates the sync daemon's configuration.
2package config
3
4import (
5 "fmt"
6 "os"
7 "time"
8
9 "gopkg.in/yaml.v3"
10)
11
12// Config is the top-level configuration for the sync daemon.
13type Config struct {
14 SyncInterval time.Duration `yaml:"sync_interval"`
15 StateDB string `yaml:"state_db"`
16 Repos []RepoPair `yaml:"repos"`
17}
18
19// RepoPair links one Forgejo repository to one Radicle repository and
20// declares which content types are kept in sync between them.
21type RepoPair struct {
22 Name string `yaml:"name"`
23 Forgejo ForgejoTarget `yaml:"forgejo"`
24 Radicle RadicleTarget `yaml:"radicle"`
25 Sync SyncScope `yaml:"sync"`
26}
27
28// ForgejoTarget identifies a repository on a Forgejo instance and where to
29// read its access token from. The token is never stored inline in the
30// config file so the file itself can be handled as non-sensitive.
31type ForgejoTarget struct {
32 BaseURL string `yaml:"base_url"`
33 Owner string `yaml:"owner"`
34 Repo string `yaml:"repo"`
35 TokenFile string `yaml:"token_file"`
36}
37
38// RadicleTarget identifies a repository on a Radicle node. HTTPBaseURL is
39// that node's radicle-httpd endpoint (e.g. https://r1.cyberwild.org), used
40// for reads; RadHome is the local RAD_HOME whose keys the "rad" CLI uses
41// for writes — the two are only the same node when the daemon runs
42// alongside it, which is not assumed here.
43type RadicleTarget struct {
44 RID string `yaml:"rid"`
45 HTTPBaseURL string `yaml:"http_base_url"`
46 RadHome string `yaml:"rad_home"`
47}
48
49// SyncScope selects which content types are synchronized for a repo pair.
50type SyncScope struct {
51 Git bool `yaml:"git"`
52 Issues bool `yaml:"issues"`
53 Patches bool `yaml:"patches"`
54}
55
56// Load reads and validates the configuration file at path.
57func Load(path string) (*Config, error) {
58 data, err := os.ReadFile(path)
59 if err != nil {
60 return nil, fmt.Errorf("read config: %w", err)
61 }
62
63 var cfg Config
64 if err := yaml.Unmarshal(data, &cfg); err != nil {
65 return nil, fmt.Errorf("parse config: %w", err)
66 }
67
68 if cfg.SyncInterval <= 0 {
69 cfg.SyncInterval = 5 * time.Minute
70 }
71 if cfg.StateDB == "" {
72 return nil, fmt.Errorf("state_db must be set")
73 }
74 if len(cfg.Repos) == 0 {
75 return nil, fmt.Errorf("no repos configured")
76 }
77 for i, r := range cfg.Repos {
78 if err := r.validate(); err != nil {
79 return nil, fmt.Errorf("repos[%d] (%s): %w", i, r.Name, err)
80 }
81 }
82
83 return &cfg, nil
84}
85
86func (r RepoPair) validate() error {
87 if r.Name == "" {
88 return fmt.Errorf("name is required")
89 }
90 if r.Forgejo.BaseURL == "" || r.Forgejo.Owner == "" || r.Forgejo.Repo == "" {
91 return fmt.Errorf("forgejo.base_url, owner and repo are required")
92 }
93 if r.Forgejo.TokenFile == "" {
94 return fmt.Errorf("forgejo.token_file is required")
95 }
96 if r.Radicle.RID == "" {
97 return fmt.Errorf("radicle.rid is required")
98 }
99 if r.Radicle.HTTPBaseURL == "" {
100 return fmt.Errorf("radicle.http_base_url is required")
101 }
102 if r.Radicle.RadHome == "" {
103 return fmt.Errorf("radicle.rad_home is required")
104 }
105 if !r.Sync.Git && !r.Sync.Issues && !r.Sync.Patches {
106 return fmt.Errorf("at least one of sync.git, sync.issues, sync.patches must be true")
107 }
108 return nil
109}
110
111// ReadToken reads a token from a file, trimming surrounding whitespace.
112// Token files are expected to be chmod 600 and owned by the service user.
113func ReadToken(path string) (string, error) {
114 data, err := os.ReadFile(path)
115 if err != nil {
116 return "", fmt.Errorf("read token file %s: %w", path, err)
117 }
118 token := string(data)
119 for len(token) > 0 && (token[len(token)-1] == '\n' || token[len(token)-1] == '\r' || token[len(token)-1] == ' ') {
120 token = token[:len(token)-1]
121 }
122 if token == "" {
123 return "", fmt.Errorf("token file %s is empty", path)
124 }
125 return token, nil
126}