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	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
129		return err
130	}
131	return os.WriteFile(path, []byte(newConfigFile(cfg)), 0o600) // nolint: errcheck
132}
133
134// DefaultConfig returns a Config with the values populated with the defaults
135// or specified environment variables.
136func DefaultConfig() *Config {
137	dataPath := os.Getenv("SOFT_SERVE_DATA_PATH")
138	if dataPath == "" {
139		dataPath = "data"
140	}
141
142	cfg := &Config{
143		Name:     "Soft Serve",
144		DataPath: dataPath,
145		SSH: SSHConfig{
146			ListenAddr:      ":23231",
147			PublicURL:       "ssh://localhost:23231",
148			KeyPath:         filepath.Join("ssh", "soft_serve_host_ed25519"),
149			ClientKeyPath:   filepath.Join("ssh", "soft_serve_client_ed25519"),
150			InternalKeyPath: filepath.Join("ssh", "soft_serve_internal_ed25519"),
151			MaxTimeout:      0,
152			IdleTimeout:     120,
153		},
154		Git: GitConfig{
155			ListenAddr:     ":9418",
156			MaxTimeout:     0,
157			IdleTimeout:    3,
158			MaxConnections: 32,
159		},
160		HTTP: HTTPConfig{
161			ListenAddr: ":8080",
162			PublicURL:  "http://localhost:8080",
163		},
164		Stats: StatsConfig{
165			ListenAddr: ":8081",
166		},
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	}
177
178	// Override with environment variables
179	if err := env.Parse(cfg, env.Options{
180		Prefix: "SOFT_SERVE_",
181	}); err != nil {
182		log.Fatal(err)
183	}
184
185	// Write config if it doesn't exist
186	if _, err := os.Stat(cp); os.IsNotExist(err) {
187		if err := WriteConfig(cp, cfg); err != nil {
188			log.Fatal("failed to write config", "err", err)
189		}
190	}
191
192	if err := cfg.init(); err != nil {
193		log.Fatal(err)
194	}
195
196	return cfg
197}
198
199// WithBackend sets the backend for the configuration.
200func (c *Config) WithBackend(backend backend.Backend) *Config {
201	c.Backend = backend
202	return c
203}
204
205func (c *Config) init() error {
206	// Use absolute paths
207	if !filepath.IsAbs(c.DataPath) {
208		dp, err := filepath.Abs(c.DataPath)
209		if err != nil {
210			return err
211		}
212		c.DataPath = dp
213	}
214
215	c.SSH.PublicURL = strings.TrimSuffix(c.SSH.PublicURL, "/")
216	c.HTTP.PublicURL = strings.TrimSuffix(c.HTTP.PublicURL, "/")
217
218	if c.SSH.KeyPath != "" && !filepath.IsAbs(c.SSH.KeyPath) {
219		c.SSH.KeyPath = filepath.Join(c.DataPath, c.SSH.KeyPath)
220	}
221
222	if c.SSH.ClientKeyPath != "" && !filepath.IsAbs(c.SSH.ClientKeyPath) {
223		c.SSH.ClientKeyPath = filepath.Join(c.DataPath, c.SSH.ClientKeyPath)
224	}
225
226	if c.SSH.InternalKeyPath != "" && !filepath.IsAbs(c.SSH.InternalKeyPath) {
227		c.SSH.InternalKeyPath = filepath.Join(c.DataPath, c.SSH.InternalKeyPath)
228	}
229
230	if c.HTTP.TLSKeyPath != "" && !filepath.IsAbs(c.HTTP.TLSKeyPath) {
231		c.HTTP.TLSKeyPath = filepath.Join(c.DataPath, c.HTTP.TLSKeyPath)
232	}
233
234	if c.HTTP.TLSCertPath != "" && !filepath.IsAbs(c.HTTP.TLSCertPath) {
235		c.HTTP.TLSCertPath = filepath.Join(c.DataPath, c.HTTP.TLSCertPath)
236	}
237
238	return nil
239}