diff --git a/server/config/config.go b/server/config/config.go index 8595fe48a..649ff7092 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,97 +1,148 @@ package config import ( + "os" "path/filepath" "github.com/caarlos0/env/v6" "github.com/charmbracelet/log" "github.com/charmbracelet/soft-serve/server/backend" + "gopkg.in/yaml.v3" ) // SSHConfig is the configuration for the SSH server. type SSHConfig struct { // ListenAddr is the address on which the SSH server will listen. - ListenAddr string `env:"LISTEN_ADDR" envDefault:":23231"` + ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"` // PublicURL is the public URL of the SSH server. - PublicURL string `env:"PUBLIC_URL" envDefault:"ssh://localhost:23231"` + PublicURL string `env:"PUBLIC_URL" yaml:"public_url"` // KeyPath is the path to the SSH server's private key. - KeyPath string `env:"KEY_PATH"` + KeyPath string `env:"KEY_PATH" yaml:"key_path"` // InternalKeyPath is the path to the SSH server's internal private key. - InternalKeyPath string `env:"INTERNAL_KEY_PATH"` + InternalKeyPath string `env:"INTERNAL_KEY_PATH" yaml:"internal_key_path"` // MaxTimeout is the maximum number of seconds a connection can take. - MaxTimeout int `env:"MAX_TIMEOUT" envDefault:"0"` + MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout` // IdleTimeout is the number of seconds a connection can be idle before it is closed. - IdleTimeout int `env:"IDLE_TIMEOUT" envDefault:"120"` + IdleTimeout int `env:"IDLE_TIMEOUT" yaml:"idle_timeout"` } // GitConfig is the Git daemon configuration for the server. type GitConfig struct { // ListenAddr is the address on which the Git daemon will listen. - ListenAddr string `env:"LISTEN_ADDR" envDefault:":9418"` + ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"` // MaxTimeout is the maximum number of seconds a connection can take. - MaxTimeout int `env:"MAX_TIMEOUT" envDefault:"0"` + MaxTimeout int `env:"MAX_TIMEOUT" yaml:"max_timeout"` // IdleTimeout is the number of seconds a connection can be idle before it is closed. - IdleTimeout int `env:"IDLE_TIMEOUT" envDefault:"3"` + IdleTimeout int `env:"IDLE_TIMEOUT" yaml:"idle_timeout"` // MaxConnections is the maximum number of concurrent connections. - MaxConnections int `env:"MAX_CONNECTIONS" envDefault:"32"` + MaxConnections int `env:"MAX_CONNECTIONS" yaml:"max_connections"` } // HTTPConfig is the HTTP configuration for the server. type HTTPConfig struct { // ListenAddr is the address on which the HTTP server will listen. - ListenAddr string `env:"LISTEN_ADDR" envDefault:":8080"` + ListenAddr string `env:"LISTEN_ADDR" yaml:"listen_addr"` // PublicURL is the public URL of the HTTP server. - PublicURL string `env:"PUBLIC_URL" envDefault:"http://localhost:8080"` + PublicURL string `env:"PUBLIC_URL" yaml:"public_url"` } // Config is the configuration for Soft Serve. type Config struct { // Name is the name of the server. - Name string `env:"NAME" envDefault:"Soft Serve"` + Name string `env:"NAME" yaml:"name"` // SSH is the configuration for the SSH server. - SSH SSHConfig `envPrefix:"SSH_"` + SSH SSHConfig `envPrefix:"SSH_" yaml:"ssh"` // Git is the configuration for the Git daemon. - Git GitConfig `envPrefix:"GIT_"` + Git GitConfig `envPrefix:"GIT_" yaml:"git"` // HTTP is the configuration for the HTTP server. - HTTP HTTPConfig `envPrefix:"HTTP_"` + HTTP HTTPConfig `envPrefix:"HTTP_" yaml:"http"` // InitialAdminKeys is a list of public keys that will be added to the list of admins. - InitialAdminKeys []string `env:"INITIAL_ADMIN_KEY" envSeparator:"\n"` + InitialAdminKeys []string `env:"INITIAL_ADMIN_KEY" envSeparator:"\n" yaml:"initial_admin_keys"` // DataPath is the path to the directory where Soft Serve will store its data. - DataPath string `env:"DATA_PATH" envDefault:"data"` + DataPath string `env:"DATA_PATH" envDefault:"data" yaml:"-"` // Backend is the Git backend to use. - Backend backend.Backend + Backend backend.Backend `yaml:"-"` +} + +// ParseConfig parses the configuration from the given file. +func ParseConfig(path string) (*Config, error) { + cfg := &Config{} + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + if err := yaml.NewDecoder(f).Decode(cfg); err != nil { + return nil, err + } + + return cfg, nil } // DefaultConfig returns a Config with the values populated with the defaults // or specified environment variables. func DefaultConfig() *Config { - cfg := &Config{} + dataPath := os.Getenv("SOFT_SERVE_DATA_PATH") + if dataPath == "" { + dataPath = "data" + } + + cfg := &Config{ + Name: "Soft Serve", + DataPath: dataPath, + SSH: SSHConfig{ + ListenAddr: ":23231", + PublicURL: "ssh://localhost:23231", + KeyPath: filepath.Join("ssh", "soft_serve"), + InternalKeyPath: filepath.Join("ssh", "soft_serve_internal"), + MaxTimeout: 0, + IdleTimeout: 120, + }, + Git: GitConfig{ + ListenAddr: ":9418", + MaxTimeout: 0, + IdleTimeout: 3, + MaxConnections: 32, + }, + HTTP: HTTPConfig{ + ListenAddr: ":8080", + PublicURL: "http://localhost:8080", + }, + } + cp := filepath.Join(cfg.DataPath, "config.yaml") + f, err := os.Open(cp) + if err == nil { + defer f.Close() + if err := yaml.NewDecoder(f).Decode(cfg); err != nil { + log.Error("failed to decode config", "err", err) + } + } else { + defer func() { + os.WriteFile(cp, []byte(newConfigFile(cfg)), 0o600) // nolint: errcheck + }() + } + if err := env.Parse(cfg, env.Options{ Prefix: "SOFT_SERVE_", }); err != nil { log.Fatal(err) } - if cfg.SSH.KeyPath == "" { - cfg.SSH.KeyPath = filepath.Join(cfg.DataPath, "ssh", "soft_serve") - } - if cfg.SSH.InternalKeyPath == "" { - cfg.SSH.InternalKeyPath = filepath.Join(cfg.DataPath, "ssh", "soft_serve_internal") - } + return cfg } diff --git a/server/config/file.go b/server/config/file.go new file mode 100644 index 000000000..00baa1315 --- /dev/null +++ b/server/config/file.go @@ -0,0 +1,68 @@ +package config + +import ( + "bytes" + "text/template" +) + +var ( + configFileTmpl = template.Must(template.New("config").Parse(`# Soft Serve Server configurations + +# The name of the server. +# This is the name that will be displayed in the UI. +name: "{{ .Name }}" + +# The SSH server configuration. +ssh: + # The address on which the SSH server will listen. + listen_addr: "{{ .SSH.ListenAddr }}" + + # The public URL of the SSH server. + # This is the address will be used to clone repositories. + public_url: "{{ .SSH.PublicURL }}" + + # The relative path to the SSH server's private key. + key_path: "{{ .SSH.KeyPath }}" + + # The relative path to the SSH server's internal api private key. + internal_key_path: "{{ .SSH.InternalKeyPath }}" + + # The maximum number of seconds a connection can take. + # A value of 0 means no timeout. + max_timeout: {{ .SSH.MaxTimeout }} + + # The number of seconds a connection can be idle before it is closed. + idle_timeout: {{ .SSH.IdleTimeout }} + +# The Git daemon configuration. +git: + # The address on which the Git daemon will listen. + listen_addr: "{{ .Git.ListenAddr }}" + + # The maximum number of seconds a connection can take. + # A value of 0 means no timeout. + max_timeout: {{ .Git.MaxTimeout }} + + # The number of seconds a connection can be idle before it is closed. + idle_timeout: {{ .Git.IdleTimeout }} + + # The maximum number of concurrent connections. + max_connections: {{ .Git.MaxConnections }} + +# The HTTP server configuration. +http: + # The address on which the HTTP server will listen. + listen_addr: "{{ .HTTP.ListenAddr }}" + + # The public URL of the HTTP server. + # This is the address will be used to clone repositories. + public_url: "{{ .HTTP.PublicURL }}" + +`)) +) + +func newConfigFile(cfg *Config) string { + var b bytes.Buffer + configFileTmpl.Execute(&b, cfg) + return b.String() +}