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}