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