Add Nix flake for mcproxyctl
Vendor dependencies and expose mcproxyctl binary via nix build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
307
vendor/git.wntrmute.dev/kyle/mcdsl/config/config.go
vendored
Normal file
307
vendor/git.wntrmute.dev/kyle/mcdsl/config/config.go
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
// Package config provides TOML configuration loading with environment
|
||||
// variable overrides for Metacircular services.
|
||||
//
|
||||
// Services define their own config struct embedding [Base], which provides
|
||||
// the standard sections (Server, Database, MCIAS, Log). Use [Load] to
|
||||
// parse a TOML file, apply environment overrides, set defaults, and
|
||||
// validate required fields.
|
||||
//
|
||||
// # Duration fields
|
||||
//
|
||||
// Timeout fields in [ServerConfig] use the [Duration] type rather than
|
||||
// [time.Duration] because go-toml v2 does not natively decode strings
|
||||
// (e.g., "30s") into time.Duration. Access the underlying value via
|
||||
// the embedded field:
|
||||
//
|
||||
// cfg.Server.ReadTimeout.Duration // time.Duration
|
||||
//
|
||||
// In TOML files, durations are written as Go duration strings:
|
||||
//
|
||||
// read_timeout = "30s"
|
||||
// idle_timeout = "2m"
|
||||
//
|
||||
// Environment variable overrides also use this format:
|
||||
//
|
||||
// MCR_SERVER_READ_TIMEOUT=30s
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcdsl/auth"
|
||||
)
|
||||
|
||||
// Base contains the configuration sections common to all Metacircular
|
||||
// services. Services embed this in their own config struct and add
|
||||
// service-specific sections.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type MyConfig struct {
|
||||
// config.Base
|
||||
// MyService MyServiceSection `toml:"my_service"`
|
||||
// }
|
||||
type Base struct {
|
||||
Server ServerConfig `toml:"server"`
|
||||
Database DatabaseConfig `toml:"database"`
|
||||
MCIAS auth.Config `toml:"mcias"`
|
||||
Log LogConfig `toml:"log"`
|
||||
}
|
||||
|
||||
// ServerConfig holds TLS server settings.
|
||||
type ServerConfig struct {
|
||||
// ListenAddr is the HTTPS listen address (e.g., ":8443"). Required.
|
||||
ListenAddr string `toml:"listen_addr"`
|
||||
|
||||
// GRPCAddr is the gRPC listen address (e.g., ":9443"). Optional;
|
||||
// gRPC is disabled if empty.
|
||||
GRPCAddr string `toml:"grpc_addr"`
|
||||
|
||||
// TLSCert is the path to the TLS certificate file (PEM). Required.
|
||||
TLSCert string `toml:"tls_cert"`
|
||||
|
||||
// TLSKey is the path to the TLS private key file (PEM). Required.
|
||||
TLSKey string `toml:"tls_key"`
|
||||
|
||||
// ReadTimeout is the maximum duration for reading the entire request.
|
||||
// Defaults to 30s.
|
||||
ReadTimeout Duration `toml:"read_timeout"`
|
||||
|
||||
// WriteTimeout is the maximum duration before timing out writes.
|
||||
// Defaults to 30s.
|
||||
WriteTimeout Duration `toml:"write_timeout"`
|
||||
|
||||
// IdleTimeout is the maximum time to wait for the next request on
|
||||
// a keep-alive connection. Defaults to 120s.
|
||||
IdleTimeout Duration `toml:"idle_timeout"`
|
||||
|
||||
// ShutdownTimeout is the maximum time to wait for in-flight requests
|
||||
// to drain during graceful shutdown. Defaults to 60s.
|
||||
ShutdownTimeout Duration `toml:"shutdown_timeout"`
|
||||
}
|
||||
|
||||
// DatabaseConfig holds SQLite database settings.
|
||||
type DatabaseConfig struct {
|
||||
// Path is the path to the SQLite database file. Required.
|
||||
Path string `toml:"path"`
|
||||
}
|
||||
|
||||
// LogConfig holds logging settings.
|
||||
type LogConfig struct {
|
||||
// Level is the log level (debug, info, warn, error). Defaults to "info".
|
||||
Level string `toml:"level"`
|
||||
}
|
||||
|
||||
// WebConfig holds web UI server settings. This is not part of Base because
|
||||
// not all services have a web UI — services that do can add it to their
|
||||
// own config struct.
|
||||
type WebConfig struct {
|
||||
// ListenAddr is the web UI listen address (e.g., "127.0.0.1:8080").
|
||||
ListenAddr string `toml:"listen_addr"`
|
||||
|
||||
// GRPCAddr is the gRPC address of the API server that the web UI
|
||||
// connects to.
|
||||
GRPCAddr string `toml:"grpc_addr"`
|
||||
|
||||
// CACert is an optional CA certificate for verifying the API server's
|
||||
// TLS certificate.
|
||||
CACert string `toml:"ca_cert"`
|
||||
}
|
||||
|
||||
// Validator is an optional interface that config structs can implement
|
||||
// to add service-specific validation. If the config type implements
|
||||
// Validator, its Validate method is called after defaults and env
|
||||
// overrides are applied.
|
||||
type Validator interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
// Load reads a TOML config file at path, applies environment variable
|
||||
// overrides using envPrefix (e.g., "MCR" maps MCR_SERVER_LISTEN_ADDR to
|
||||
// Server.ListenAddr), sets defaults for unset optional fields, and
|
||||
// validates required fields.
|
||||
//
|
||||
// If T implements [Validator], its Validate method is called after all
|
||||
// other processing.
|
||||
func Load[T any](path string, envPrefix string) (*T, error) {
|
||||
data, err := os.ReadFile(path) //nolint:gosec // config path is operator-supplied
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config: read %s: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg T
|
||||
if err := toml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("config: parse %s: %w", path, err)
|
||||
}
|
||||
|
||||
if envPrefix != "" {
|
||||
applyEnvToStruct(reflect.ValueOf(&cfg).Elem(), envPrefix)
|
||||
}
|
||||
|
||||
applyBaseDefaults(&cfg)
|
||||
|
||||
if err := validateBase(&cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := any(&cfg).(Validator); ok {
|
||||
if err := v.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// applyBaseDefaults sets defaults on the embedded Base struct if present.
|
||||
func applyBaseDefaults(cfg any) {
|
||||
base := findBase(cfg)
|
||||
if base == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if base.Server.ReadTimeout.Duration == 0 {
|
||||
base.Server.ReadTimeout.Duration = 30 * time.Second
|
||||
}
|
||||
if base.Server.WriteTimeout.Duration == 0 {
|
||||
base.Server.WriteTimeout.Duration = 30 * time.Second
|
||||
}
|
||||
if base.Server.IdleTimeout.Duration == 0 {
|
||||
base.Server.IdleTimeout.Duration = 120 * time.Second
|
||||
}
|
||||
if base.Server.ShutdownTimeout.Duration == 0 {
|
||||
base.Server.ShutdownTimeout.Duration = 60 * time.Second
|
||||
}
|
||||
if base.Log.Level == "" {
|
||||
base.Log.Level = "info"
|
||||
}
|
||||
}
|
||||
|
||||
// validateBase checks required fields on the embedded Base struct if present.
|
||||
func validateBase(cfg any) error {
|
||||
base := findBase(cfg)
|
||||
if base == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
required := []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"server.listen_addr", base.Server.ListenAddr},
|
||||
{"server.tls_cert", base.Server.TLSCert},
|
||||
{"server.tls_key", base.Server.TLSKey},
|
||||
}
|
||||
|
||||
for _, r := range required {
|
||||
if r.value == "" {
|
||||
return fmt.Errorf("config: required field %q is missing", r.name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findBase returns a pointer to the embedded Base struct, or nil if the
|
||||
// config type does not embed Base.
|
||||
func findBase(cfg any) *Base {
|
||||
v := reflect.ValueOf(cfg)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if cfg *is* a Base.
|
||||
if b, ok := v.Addr().Interface().(*Base); ok {
|
||||
return b
|
||||
}
|
||||
|
||||
// Check embedded fields.
|
||||
t := v.Type()
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
if field.Anonymous && field.Type == reflect.TypeOf(Base{}) {
|
||||
b, ok := v.Field(i).Addr().Interface().(*Base)
|
||||
if ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyEnvToStruct recursively walks a struct and overrides field values
|
||||
// from environment variables. The env variable name is built from the
|
||||
// prefix and the toml tag: PREFIX_SECTION_FIELD (uppercased).
|
||||
//
|
||||
// Supported field types: string, time.Duration (as int64), []string
|
||||
// (comma-separated), bool, int.
|
||||
func applyEnvToStruct(v reflect.Value, prefix string) {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
t := v.Type()
|
||||
|
||||
for i := range t.NumField() {
|
||||
field := t.Field(i)
|
||||
fv := v.Field(i)
|
||||
|
||||
// For anonymous (embedded) fields, recurse with the same prefix.
|
||||
if field.Anonymous {
|
||||
applyEnvToStruct(fv, prefix)
|
||||
continue
|
||||
}
|
||||
|
||||
tag := field.Tag.Get("toml")
|
||||
if tag == "" || tag == "-" {
|
||||
continue
|
||||
}
|
||||
envKey := prefix + "_" + strings.ToUpper(tag)
|
||||
|
||||
// Handle Duration wrapper before generic struct recursion.
|
||||
if field.Type == reflect.TypeOf(Duration{}) {
|
||||
envVal, ok := os.LookupEnv(envKey)
|
||||
if ok {
|
||||
d, parseErr := time.ParseDuration(envVal)
|
||||
if parseErr == nil {
|
||||
fv.Set(reflect.ValueOf(Duration{d}))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
applyEnvToStruct(fv, envKey)
|
||||
continue
|
||||
}
|
||||
|
||||
envVal, ok := os.LookupEnv(envKey)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch fv.Kind() {
|
||||
case reflect.String:
|
||||
fv.SetString(envVal)
|
||||
case reflect.Bool:
|
||||
fv.SetBool(envVal == "true" || envVal == "1")
|
||||
case reflect.Slice:
|
||||
if field.Type.Elem().Kind() == reflect.String {
|
||||
parts := strings.Split(envVal, ",")
|
||||
for j := range parts {
|
||||
parts[j] = strings.TrimSpace(parts[j])
|
||||
}
|
||||
fv.Set(reflect.ValueOf(parts))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
vendor/git.wntrmute.dev/kyle/mcdsl/config/duration.go
vendored
Normal file
37
vendor/git.wntrmute.dev/kyle/mcdsl/config/duration.go
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Duration is a [time.Duration] that can be unmarshalled from a TOML string
|
||||
// (e.g., "30s", "5m"). go-toml v2 does not natively decode strings into
|
||||
// time.Duration, so this wrapper implements [encoding.TextUnmarshaler].
|
||||
//
|
||||
// Access the underlying time.Duration via the embedded field:
|
||||
//
|
||||
// cfg.Server.ReadTimeout.Duration // time.Duration value
|
||||
//
|
||||
// Duration values work directly with time functions that accept
|
||||
// time.Duration because of the embedding:
|
||||
//
|
||||
// time.After(cfg.Server.ReadTimeout.Duration)
|
||||
type Duration struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler for TOML string decoding.
|
||||
func (d *Duration) UnmarshalText(text []byte) error {
|
||||
parsed, err := time.ParseDuration(string(text))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid duration %q: %w", string(text), err)
|
||||
}
|
||||
d.Duration = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler for TOML string encoding.
|
||||
func (d Duration) MarshalText() ([]byte, error) {
|
||||
return []byte(d.String()), nil
|
||||
}
|
||||
Reference in New Issue
Block a user