1package config
2
3import (
4 "fmt"
5 "log"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 "github.com/caarlos0/env/v8"
12 "github.com/charmbracelet/soft-serve/server/sshutils"
13 "golang.org/x/crypto/ssh"
14 "gopkg.in/yaml.v3"
15)
16
17// Config is the configuration for Soft Serve.
18type Config struct {
19 // Name is the name of the server.
20 Name string `env:"NAME" yaml:"name"`
21
22 // SSH is the configuration for the SSH server.
23 SSH SSHConfig `envPrefix:"SSH_" yaml:"ssh"`
24
25 // GitDaemon is the configuration for the GitDaemon daemon.
26 GitDaemon GitDaemonConfig `envPrefix:"GIT_DAEMON_" yaml:"git_daemon"`
27
28 // HTTP is the configuration for the HTTP server.
29 HTTP HTTPConfig `envPrefix:"HTTP_" yaml:"http"`
30
31 // Stats is the configuration for the stats server.
32 Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"`
33
34 // Log is the logger configuration.
35 Log LogConfig `envPrefix:"LOG_" yaml:"log"`
36
37 // Cache is the cache backend to use.
38 Cache CacheConfig `env:"CACHE" yaml:"cache"`
39
40 // Database is the database configuration.
41 Database DatabaseConfig `envPrefix:"DATABASE_" yaml:"database"`
42
43 // Backend is the backend to use.
44 Backend BackendConfig `envPrefix:"BACKEND_" yaml:"backend"`
45
46 // InitialAdminKeys is a list of public keys that will be added to the list of admins.
47 InitialAdminKeys []string `env:"INITIAL_ADMIN_KEYS" envSeparator:"\n" yaml:"initial_admin_keys"`
48
49 // DataPath is the path to the directory where Soft Serve will store its data.
50 DataPath string `env:"DATA_PATH" yaml:"-"`
51}
52
53// Environ returns the config as a list of environment variables.
54// TODO: use pointer receiver
55func (c *Config) Environ() []string {
56 envs := append([]string{},
57 "SOFT_SERVE_NAME="+c.Name,
58 "SOFT_SERVE_DATA_PATH="+c.DataPath,
59 "SOFT_SERVE_INITIAL_ADMIN_KEYS="+strings.Join(c.InitialAdminKeys, "\n"),
60 )
61
62 envs = append(envs, c.SSH.Environ()...)
63 envs = append(envs, c.GitDaemon.Environ()...)
64 envs = append(envs, c.HTTP.Environ()...)
65 envs = append(envs, c.Stats.Environ()...)
66 envs = append(envs, c.Log.Environ()...)
67 envs = append(envs, c.Cache.Environ()...)
68 envs = append(envs, c.Database.Environ()...)
69 envs = append(envs, c.Backend.Environ()...)
70
71 return envs
72}
73
74func parseFile(v interface{}, path string) error {
75 f, err := os.Open(path)
76 if err != nil {
77 return fmt.Errorf("open config file: %w", err)
78 }
79
80 defer f.Close() // nolint: errcheck
81 if err := yaml.NewDecoder(f).Decode(v); err != nil {
82 return fmt.Errorf("decode config: %w", err)
83 }
84
85 return nil
86}
87
88func parseEnv(v interface{}) error {
89 // Override with environment variables
90 if err := env.ParseWithOptions(v, env.Options{
91 Prefix: "SOFT_SERVE_",
92 }); err != nil {
93 return fmt.Errorf("parse environment variables: %w", err)
94 }
95
96 return nil
97}
98
99// ParseConfig parses the configuration file to server configuration.
100func ParseConfig(c *Config, path string) error {
101 return parseConfig(c, path)
102}
103
104func parseConfig(cfg *Config, path string) error {
105 if cfg == nil {
106 cfg = DefaultConfig()
107 }
108
109 if path != "" {
110 // TODO: make config aware of config.yaml path
111 cfg.DataPath = filepath.Dir(path)
112 }
113
114 exist := cfg.Exist()
115 if exist {
116 if err := parseFile(cfg, cfg.FilePath()); err != nil {
117 return err
118 }
119 }
120
121 // Merge initial admin keys from both config file and environment variables.
122 initialAdminKeys := append([]string{}, cfg.InitialAdminKeys...)
123
124 if err := parseEnv(cfg); err != nil {
125 return err
126 }
127
128 // Merge initial admin keys from environment variables.
129 if initialAdminKeysEnv := os.Getenv("SOFT_SERVE_INITIAL_ADMIN_KEYS"); initialAdminKeysEnv != "" {
130 cfg.InitialAdminKeys = append(cfg.InitialAdminKeys, initialAdminKeys...)
131 }
132
133 // Validate keys
134 pks := make([]string, 0)
135 for _, key := range parseAuthKeys(cfg.InitialAdminKeys) {
136 ak := sshutils.MarshalAuthorizedKey(key)
137 pks = append(pks, ak)
138 }
139
140 cfg.InitialAdminKeys = pks
141
142 if err := cfg.validate(); err != nil {
143 return err
144 }
145
146 return nil
147}
148
149// DefaultConfig returns the default config.
150func DefaultConfig() *Config {
151 dataPath := os.Getenv("SOFT_SERVE_DATA_PATH")
152 if dataPath == "" {
153 dataPath = "data"
154 }
155
156 return &Config{
157 Name: "Soft Serve",
158 DataPath: dataPath,
159 InitialAdminKeys: []string{},
160 SSH: SSHConfig{
161 ListenAddr: ":23231",
162 PublicURL: "ssh://localhost:23231",
163 KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"),
164 ClientKeyPath: filepath.Join("ssh", "soft_serve_client_ed25519"),
165 MaxTimeout: 0,
166 IdleTimeout: 0,
167 },
168 GitDaemon: GitDaemonConfig{
169 ListenAddr: ":9418",
170 MaxTimeout: 0,
171 IdleTimeout: 3,
172 MaxConnections: 32,
173 },
174 HTTP: HTTPConfig{
175 ListenAddr: ":23232",
176 PublicURL: "http://localhost:23232",
177 },
178 Stats: StatsConfig{
179 ListenAddr: "localhost:23233",
180 },
181 Log: LogConfig{
182 Format: "text",
183 TimeFormat: time.DateTime,
184 },
185 Cache: CacheConfig{
186 Backend: "lru",
187 },
188 Database: DatabaseConfig{
189 Driver: "sqlite",
190 DataSource: "soft-serve.db",
191 },
192 Backend: BackendConfig{
193 Settings: "sqlite",
194 Access: "sqlite",
195 Auth: "sqlite",
196 Store: "sqlite",
197 },
198 }
199}
200
201// FilePath returns the expected config file path.
202func (c *Config) FilePath() string {
203 return filepath.Join(c.DataPath, "config.yaml")
204}
205
206// Exist returns true if the configuration file exists.
207func (c *Config) Exist() bool {
208 _, err := os.Stat(c.FilePath())
209 return err == nil
210}
211
212// ReadConfig parses the configuration file.
213func (c *Config) ReadConfig() error {
214 return parseConfig(c, c.FilePath())
215}
216
217// WriteConfig writes the configuration in the default path.
218func (c *Config) WriteConfig() error {
219 return WriteConfig(c)
220}
221
222// ReposPath returns the expected repositories path.
223func (c *Config) ReposPath() string {
224 return filepath.Join(c.DataPath, "repos")
225}
226
227// WriteConfig writes the configuration in the default path.
228func WriteConfig(c *Config) error {
229 if c == nil {
230 return fmt.Errorf("nil config")
231 }
232
233 fp := c.FilePath()
234 if err := os.MkdirAll(filepath.Dir(fp), os.ModePerm); err != nil {
235 return err
236 }
237
238 return os.WriteFile(fp, []byte(newConfigFile(c)), 0o644) // nolint: errcheck
239}
240
241func (c *Config) validate() error {
242 // Use absolute paths
243 if !filepath.IsAbs(c.DataPath) {
244 dp, err := filepath.Abs(c.DataPath)
245 if err != nil {
246 return err
247 }
248 c.DataPath = dp
249 }
250
251 c.SSH.PublicURL = strings.TrimSuffix(c.SSH.PublicURL, "/")
252
253 if c.SSH.KeyPath != "" && !filepath.IsAbs(c.SSH.KeyPath) {
254 c.SSH.KeyPath = filepath.Join(c.DataPath, c.SSH.KeyPath)
255 }
256
257 if c.SSH.ClientKeyPath != "" && !filepath.IsAbs(c.SSH.ClientKeyPath) {
258 c.SSH.ClientKeyPath = filepath.Join(c.DataPath, c.SSH.ClientKeyPath)
259 }
260
261 c.HTTP.PublicURL = strings.TrimSuffix(c.HTTP.PublicURL, "/")
262
263 if c.HTTP.TLSKeyPath != "" && !filepath.IsAbs(c.HTTP.TLSKeyPath) {
264 c.HTTP.TLSKeyPath = filepath.Join(c.DataPath, c.HTTP.TLSKeyPath)
265 }
266
267 if c.HTTP.TLSCertPath != "" && !filepath.IsAbs(c.HTTP.TLSCertPath) {
268 c.HTTP.TLSCertPath = filepath.Join(c.DataPath, c.HTTP.TLSCertPath)
269 }
270
271 switch c.Database.Driver {
272 case "sqlite":
273 if c.Database.DataSource != "" && !filepath.IsAbs(c.Database.DataSource) {
274 c.Database.DataSource = filepath.Join(c.DataPath, c.Database.DataSource)
275 }
276 }
277
278 return nil
279}
280
281// parseAuthKeys parses authorized keys from either file paths or string authorized_keys.
282func parseAuthKeys(aks []string) []ssh.PublicKey {
283 pks := make([]ssh.PublicKey, 0)
284 for _, key := range aks {
285 if bts, err := os.ReadFile(key); err == nil {
286 // key is a file
287 key = strings.TrimSpace(string(bts))
288 }
289 if pk, _, err := sshutils.ParseAuthorizedKey(key); err == nil {
290 pks = append(pks, pk)
291 }
292 }
293 return pks
294}
295
296// AdminKeys returns the server admin keys.
297func (c *Config) AdminKeys() []ssh.PublicKey {
298 if c.InitialAdminKeys == nil {
299 return []ssh.PublicKey{}
300 }
301
302 log.Print(c.InitialAdminKeys)
303 return parseAuthKeys(c.InitialAdminKeys)
304}