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