Custom config package with optional TLS fields. When tls_cert/tls_key are empty, serves plain HTTP (behind mc-proxy which terminates TLS). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
3.1 KiB
Go
139 lines
3.1 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pelletier/go-toml/v2"
|
|
|
|
mcdslauth "git.wntrmute.dev/mc/mcdsl/auth"
|
|
)
|
|
|
|
// Config is the MCQ configuration.
|
|
type Config struct {
|
|
Server ServerConfig `toml:"server"`
|
|
Database DatabaseConfig `toml:"database"`
|
|
MCIAS mcdslauth.Config `toml:"mcias"`
|
|
Log LogConfig `toml:"log"`
|
|
}
|
|
|
|
// ServerConfig holds HTTP/gRPC server settings. TLS fields are optional;
|
|
// when empty, MCQ serves plain HTTP (for use behind mc-proxy L7).
|
|
type ServerConfig struct {
|
|
ListenAddr string `toml:"listen_addr"`
|
|
GRPCAddr string `toml:"grpc_addr"`
|
|
TLSCert string `toml:"tls_cert"`
|
|
TLSKey string `toml:"tls_key"`
|
|
ReadTimeout Duration `toml:"read_timeout"`
|
|
WriteTimeout Duration `toml:"write_timeout"`
|
|
IdleTimeout Duration `toml:"idle_timeout"`
|
|
ShutdownTimeout Duration `toml:"shutdown_timeout"`
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
Path string `toml:"path"`
|
|
}
|
|
|
|
type LogConfig struct {
|
|
Level string `toml:"level"`
|
|
}
|
|
|
|
// Duration wraps time.Duration for TOML string parsing.
|
|
type Duration struct {
|
|
time.Duration
|
|
}
|
|
|
|
func (d *Duration) UnmarshalText(text []byte) error {
|
|
var err error
|
|
d.Duration, err = time.ParseDuration(string(text))
|
|
return err
|
|
}
|
|
|
|
// Load reads, parses, and validates a config file.
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path) // #nosec G304 -- config path is operator-controlled
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config: %w", err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
Server: ServerConfig{
|
|
ListenAddr: ":8080",
|
|
ReadTimeout: Duration{30 * time.Second},
|
|
WriteTimeout: Duration{30 * time.Second},
|
|
IdleTimeout: Duration{120 * time.Second},
|
|
ShutdownTimeout: Duration{60 * time.Second},
|
|
},
|
|
Log: LogConfig{Level: "info"},
|
|
}
|
|
|
|
if err := toml.Unmarshal(data, cfg); err != nil {
|
|
return nil, fmt.Errorf("parse config: %w", err)
|
|
}
|
|
|
|
applyEnvOverrides(cfg)
|
|
|
|
if err := cfg.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c *Config) validate() error {
|
|
if c.Server.ListenAddr == "" {
|
|
return fmt.Errorf("config: server.listen_addr is required")
|
|
}
|
|
if c.Database.Path == "" {
|
|
return fmt.Errorf("config: database.path is required")
|
|
}
|
|
if c.MCIAS.ServerURL == "" {
|
|
return fmt.Errorf("config: mcias.server_url is required")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyEnvOverrides(cfg *Config) {
|
|
if port := os.Getenv("PORT"); port != "" {
|
|
cfg.Server.ListenAddr = ":" + port
|
|
}
|
|
applyEnvToStruct("MCQ", reflect.ValueOf(cfg).Elem())
|
|
}
|
|
|
|
func applyEnvToStruct(prefix string, v reflect.Value) {
|
|
t := v.Type()
|
|
for i := range t.NumField() {
|
|
field := t.Field(i)
|
|
fv := v.Field(i)
|
|
|
|
tag := field.Tag.Get("toml")
|
|
if tag == "" || tag == "-" {
|
|
continue
|
|
}
|
|
|
|
envKey := prefix + "_" + strings.ToUpper(tag)
|
|
|
|
if fv.Kind() == reflect.Struct && field.Type != reflect.TypeOf(Duration{}) {
|
|
applyEnvToStruct(envKey, fv)
|
|
continue
|
|
}
|
|
|
|
envVal := os.Getenv(envKey)
|
|
if envVal == "" {
|
|
continue
|
|
}
|
|
|
|
if fv.Kind() == reflect.String {
|
|
fv.SetString(envVal)
|
|
}
|
|
if field.Type == reflect.TypeOf(Duration{}) {
|
|
if d, err := time.ParseDuration(envVal); err == nil {
|
|
fv.Set(reflect.ValueOf(Duration{d}))
|
|
}
|
|
}
|
|
}
|
|
}
|