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