1package config
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9
10 "github.com/caarlos0/env/v7"
11 "github.com/charmbracelet/log"
12 "github.com/charmbracelet/soft-serve/server/backend"
13 "gopkg.in/yaml.v3"
14)
15
16// SSHConfig is the configuration for the SSH server.
17type SSHConfig struct {
18 // ListenAddr is the address on which the SSH server will listen.
19 ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
20
21 // PublicURL is the public URL of the SSH server.
22 PublicURL string `env:"PUBLIC_URL" yaml:"public_url"`
23
24 // KeyPath is the path to the SSH server's private key.
25 KeyPath string `env:"KEY_PATH" yaml:"key_path"`
26
27 // ClientKeyPath is the path to the SSH server's client private key.
28 ClientKeyPath string `env:"CLIENT_KEY_PATH" yaml:"client_key_path"`
29
30 // InternalKeyPath is the path to the SSH server's internal private key.
31 InternalKeyPath string `env:"INTERNAL_KEY_PATH" yaml:"internal_key_path"`
32
33 // MaxTimeout is the maximum number of seconds a connection can take.
34 MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout`
35
36 // IdleTimeout is the number of seconds a connection can be idle before it is closed.
37 IdleTimeout int `env:"IDLE_TIMEOUT" yaml:"idle_timeout"`
38}
39
40// GitConfig is the Git daemon configuration for the server.
41type GitConfig struct {
42 // ListenAddr is the address on which the Git daemon will listen.
43 ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
44
45 // MaxTimeout is the maximum number of seconds a connection can take.
46 MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout"`
47
48 // IdleTimeout is the number of seconds a connection can be idle before it is closed.
49 IdleTimeout int `env:"IDLE_TIMEOUT" yaml:"idle_timeout"`
50
51 // MaxConnections is the maximum number of concurrent connections.
52 MaxConnections int `env:"MAX_CONNECTIONS" yaml:"max_connections"`
53}
54
55// HTTPConfig is the HTTP configuration for the server.
56type HTTPConfig struct {
57 // ListenAddr is the address on which the HTTP server will listen.
58 ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
59
60 // TLSKeyPath is the path to the TLS private key.
61 TLSKeyPath string `env:"TLS_KEY_PATH" yaml:"tls_key_path"`
62
63 // TLSCertPath is the path to the TLS certificate.
64 TLSCertPath string `env:"TLS_CERT_PATH" yaml:"tls_cert_path"`
65
66 // PublicURL is the public URL of the HTTP server.
67 PublicURL string `env:"PUBLIC_URL" yaml:"public_url"`
68}
69
70// StatsConfig is the configuration for the stats server.
71type StatsConfig struct {
72 // ListenAddr is the address on which the stats server will listen.
73 ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"`
74}
75
76// Config is the configuration for Soft Serve.
77type Config struct {
78 // Name is the name of the server.
79 Name string `env:"NAME" yaml:"name"`
80
81 // SSH is the configuration for the SSH server.
82 SSH SSHConfig `envPrefix:"SSH_" yaml:"ssh"`
83
84 // Git is the configuration for the Git daemon.
85 Git GitConfig `envPrefix:"GIT_" yaml:"git"`
86
87 // HTTP is the configuration for the HTTP server.
88 HTTP HTTPConfig `envPrefix:"HTTP_" yaml:"http"`
89
90 // Stats is the configuration for the stats server.
91 Stats StatsConfig `envPrefix:"STATS_" yaml:"stats"`
92
93 // InitialAdminKeys is a list of public keys that will be added to the list of admins.
94 InitialAdminKeys []string `env:"INITIAL_ADMIN_KEYS" envSeparator:"\n" yaml:"initial_admin_keys"`
95
96 // DataPath is the path to the directory where Soft Serve will store its data.
97 DataPath string `env:"DATA_PATH" yaml:"-"`
98
99 // Backend is the Git backend to use.
100 Backend backend.Backend `yaml:"-"`
101
102 // InternalPublicKey is the public key of the internal SSH key.
103 InternalPublicKey string `yaml:"-"`
104
105 // ClientPublicKey is the public key of the client SSH key.
106 ClientPublicKey string `yaml:"-"`
107}
108
109func parseConfig(path string) (*Config, error) {
110 dataPath := filepath.Dir(path)
111 cfg := &Config{
112 Name: "Soft Serve",
113 DataPath: dataPath,
114 SSH: SSHConfig{
115 ListenAddr: ":23231",
116 PublicURL: "ssh://localhost:23231",
117 KeyPath: filepath.Join("ssh", "soft_serve_host_ed25519"),
118 ClientKeyPath: filepath.Join("ssh", "soft_serve_client_ed25519"),
119 InternalKeyPath: filepath.Join("ssh", "soft_serve_internal_ed25519"),
120 MaxTimeout: 0,
121 IdleTimeout: 120,
122 },
123 Git: GitConfig{
124 ListenAddr: ":9418",
125 MaxTimeout: 0,
126 IdleTimeout: 3,
127 MaxConnections: 32,
128 },
129 HTTP: HTTPConfig{
130 ListenAddr: ":8080",
131 PublicURL: "http://localhost:8080",
132 },
133 Stats: StatsConfig{
134 ListenAddr: ":8081",
135 },
136 }
137
138 f, err := os.Open(path)
139 if err == nil {
140 defer f.Close() // nolint: errcheck
141 if err := yaml.NewDecoder(f).Decode(cfg); err != nil {
142 return cfg, fmt.Errorf("decode config: %w", err)
143 }
144 }
145
146 // Override with environment variables
147 if err := env.Parse(cfg, env.Options{
148 Prefix: "SOFT_SERVE_",
149 }); err != nil {
150 return cfg, fmt.Errorf("parse environment variables: %w", err)
151 }
152
153 for _, key := range cfg.InitialAdminKeys {
154 if _, _, err := backend.ParseAuthorizedKey(key); err != nil {
155 log.Error("invalid initial admin key", "err", err)
156 }
157 log.Debugf("found initial admin key: %q", key)
158 }
159
160 // Reset datapath to config dir.
161 // This is necessary because the environment variable may be set to
162 // a different directory.
163 cfg.DataPath = dataPath
164
165 return cfg, nil
166}
167
168// ParseConfig parses the configuration from the given file.
169func ParseConfig(path string) (*Config, error) {
170 cfg, err := parseConfig(path)
171 if err != nil {
172 return nil, err
173 }
174
175 if err := cfg.validate(); err != nil {
176 return nil, err
177 }
178
179 return cfg, nil
180}
181
182// WriteConfig writes the configuration to the given file.
183func WriteConfig(path string, cfg *Config) error {
184 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
185 return err
186 }
187 return os.WriteFile(path, []byte(newConfigFile(cfg)), 0o600) // nolint: errcheck
188}
189
190// DefaultConfig returns a Config with the values populated with the defaults
191// or specified environment variables.
192func DefaultConfig() *Config {
193 dataPath := os.Getenv("SOFT_SERVE_DATA_PATH")
194 if dataPath == "" {
195 dataPath = "data"
196 }
197
198 cp := filepath.Join(dataPath, "config.yaml")
199 cfg, err := parseConfig(cp)
200 if err != nil && !errors.Is(err, os.ErrNotExist) {
201 log.Errorf("failed to parse config: %v", err)
202 }
203
204 // Write config if it doesn't exist
205 if _, err := os.Stat(cp); os.IsNotExist(err) {
206 if err := WriteConfig(cp, cfg); err != nil {
207 log.Fatal("failed to write config", "err", err)
208 }
209 }
210
211 if err := cfg.validate(); err != nil {
212 log.Fatal(err)
213 }
214
215 return cfg
216}
217
218// WithBackend sets the backend for the configuration.
219func (c *Config) WithBackend(backend backend.Backend) *Config {
220 c.Backend = backend
221 return c
222}
223
224func (c *Config) validate() error {
225 // Use absolute paths
226 if !filepath.IsAbs(c.DataPath) {
227 dp, err := filepath.Abs(c.DataPath)
228 if err != nil {
229 return err
230 }
231 c.DataPath = dp
232 }
233
234 c.SSH.PublicURL = strings.TrimSuffix(c.SSH.PublicURL, "/")
235 c.HTTP.PublicURL = strings.TrimSuffix(c.HTTP.PublicURL, "/")
236
237 if c.SSH.KeyPath != "" && !filepath.IsAbs(c.SSH.KeyPath) {
238 c.SSH.KeyPath = filepath.Join(c.DataPath, c.SSH.KeyPath)
239 }
240
241 if c.SSH.ClientKeyPath != "" && !filepath.IsAbs(c.SSH.ClientKeyPath) {
242 c.SSH.ClientKeyPath = filepath.Join(c.DataPath, c.SSH.ClientKeyPath)
243 }
244
245 if c.SSH.InternalKeyPath != "" && !filepath.IsAbs(c.SSH.InternalKeyPath) {
246 c.SSH.InternalKeyPath = filepath.Join(c.DataPath, c.SSH.InternalKeyPath)
247 }
248
249 if c.HTTP.TLSKeyPath != "" && !filepath.IsAbs(c.HTTP.TLSKeyPath) {
250 c.HTTP.TLSKeyPath = filepath.Join(c.DataPath, c.HTTP.TLSKeyPath)
251 }
252
253 if c.HTTP.TLSCertPath != "" && !filepath.IsAbs(c.HTTP.TLSCertPath) {
254 c.HTTP.TLSCertPath = filepath.Join(c.DataPath, c.HTTP.TLSCertPath)
255 }
256
257 return nil
258}