Migrate db, auth, and config to mcdsl

- db.Open: delegate to mcdsl/db.Open
- db.Migrate: rewrite migrations as mcdsl/db.Migration SQL strings,
  delegate to mcdsl/db.Migrate; keep SchemaVersion via mcdsl
- auth: thin shim wrapping mcdsl/auth.Authenticator, keeps Claims
  type (with Subject, AccountType, Roles) for policy engine compat;
  delete cache.go (handled by mcdsl/auth); add ErrForbidden
- config: embed mcdsl/config.Base for standard sections (Server with
  Duration fields, Database, MCIAS, Log); keep StorageConfig and
  WebConfig as MCR-specific; use mcdsl/config.Load[T] + Validator
- WriteTimeout now defaults to 30s (mcdsl default, was 0)
- All existing tests pass (auth tests rewritten for new shim API,
  cache expiry test removed — caching tested in mcdsl)
- Net -464 lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 17:10:46 -07:00
parent 593da3975d
commit 78f3eae651
11 changed files with 179 additions and 643 deletions

View File

@@ -4,122 +4,61 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"time"
"github.com/pelletier/go-toml/v2"
mcdslconfig "git.wntrmute.dev/kyle/mcdsl/config"
)
// Config is the top-level MCR configuration.
// Config is the top-level MCR configuration. It embeds config.Base for
// the standard Metacircular sections and adds MCR-specific sections.
type Config struct {
Server ServerConfig `toml:"server"`
Database DatabaseConfig `toml:"database"`
Storage StorageConfig `toml:"storage"`
MCIAS MCIASConfig `toml:"mcias"`
Web WebConfig `toml:"web"`
Log LogConfig `toml:"log"`
}
type ServerConfig struct {
ListenAddr string `toml:"listen_addr"`
GRPCAddr string `toml:"grpc_addr"`
TLSCert string `toml:"tls_cert"`
TLSKey string `toml:"tls_key"`
ReadTimeout time.Duration `toml:"read_timeout"`
WriteTimeout time.Duration `toml:"write_timeout"`
IdleTimeout time.Duration `toml:"idle_timeout"`
ShutdownTimeout time.Duration `toml:"shutdown_timeout"`
}
type DatabaseConfig struct {
Path string `toml:"path"`
mcdslconfig.Base
Storage StorageConfig `toml:"storage"`
Web WebConfig `toml:"web"`
}
// StorageConfig holds blob/layer storage settings.
type StorageConfig struct {
LayersPath string `toml:"layers_path"`
UploadsPath string `toml:"uploads_path"`
}
type MCIASConfig struct {
ServerURL string `toml:"server_url"`
CACert string `toml:"ca_cert"`
ServiceName string `toml:"service_name"`
Tags []string `toml:"tags"`
}
// WebConfig holds the web UI server settings.
type WebConfig struct {
ListenAddr string `toml:"listen_addr"`
GRPCAddr string `toml:"grpc_addr"`
CACert string `toml:"ca_cert"`
}
type LogConfig struct {
Level string `toml:"level"`
}
// Load reads a TOML config file, applies environment variable overrides,
// sets defaults, and validates required fields.
// Load reads a TOML config file, applies environment variable overrides
// (MCR_ prefix), sets defaults, and validates required fields.
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path) //nolint:gosec // config path is operator-supplied, not user input
cfg, err := mcdslconfig.Load[Config](path, "MCR")
if err != nil {
return nil, fmt.Errorf("config: read %s: %w", path, err)
}
var cfg Config
if err := toml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("config: parse %s: %w", path, err)
}
applyEnvOverrides(&cfg)
applyDefaults(&cfg)
if err := validate(&cfg); err != nil {
return nil, err
}
return &cfg, nil
return cfg, nil
}
func applyDefaults(cfg *Config) {
if cfg.Server.ReadTimeout == 0 {
cfg.Server.ReadTimeout = 30 * time.Second
// Validate implements the mcdsl config.Validator interface. It checks
// MCR-specific required fields and constraints beyond what config.Base
// validates.
func (c *Config) Validate() error {
if c.Database.Path == "" {
return fmt.Errorf("database.path is required")
}
// WriteTimeout defaults to 0 (disabled) — no action needed.
if cfg.Server.IdleTimeout == 0 {
cfg.Server.IdleTimeout = 120 * time.Second
if c.Storage.LayersPath == "" {
return fmt.Errorf("storage.layers_path is required")
}
if cfg.Server.ShutdownTimeout == 0 {
cfg.Server.ShutdownTimeout = 60 * time.Second
}
if cfg.Storage.UploadsPath == "" && cfg.Storage.LayersPath != "" {
cfg.Storage.UploadsPath = filepath.Join(filepath.Dir(cfg.Storage.LayersPath), "uploads")
}
if cfg.Log.Level == "" {
cfg.Log.Level = "info"
}
}
func validate(cfg *Config) error {
required := []struct {
name string
value string
}{
{"server.listen_addr", cfg.Server.ListenAddr},
{"server.tls_cert", cfg.Server.TLSCert},
{"server.tls_key", cfg.Server.TLSKey},
{"database.path", cfg.Database.Path},
{"storage.layers_path", cfg.Storage.LayersPath},
{"mcias.server_url", cfg.MCIAS.ServerURL},
if c.MCIAS.ServerURL == "" {
return fmt.Errorf("mcias.server_url is required")
}
for _, r := range required {
if r.value == "" {
return fmt.Errorf("config: required field %q is missing", r.name)
}
// Default uploads path to sibling of layers path.
if c.Storage.UploadsPath == "" && c.Storage.LayersPath != "" {
c.Storage.UploadsPath = filepath.Join(filepath.Dir(c.Storage.LayersPath), "uploads")
}
return validateSameFilesystem(cfg.Storage.LayersPath, cfg.Storage.UploadsPath)
return validateSameFilesystem(c.Storage.LayersPath, c.Storage.UploadsPath)
}
// validateSameFilesystem checks that two paths reside on the same filesystem
@@ -128,16 +67,16 @@ func validate(cfg *Config) error {
func validateSameFilesystem(layersPath, uploadsPath string) error {
layersDev, err := deviceID(layersPath)
if err != nil {
return fmt.Errorf("config: stat layers_path: %w", err)
return fmt.Errorf("stat layers_path: %w", err)
}
uploadsDev, err := deviceID(uploadsPath)
if err != nil {
return fmt.Errorf("config: stat uploads_path: %w", err)
return fmt.Errorf("stat uploads_path: %w", err)
}
if layersDev != uploadsDev {
return fmt.Errorf("config: storage.layers_path and storage.uploads_path must be on the same filesystem")
return fmt.Errorf("storage.layers_path and storage.uploads_path must be on the same filesystem")
}
return nil
@@ -162,51 +101,3 @@ func deviceID(path string) (uint64, error) {
p = parent
}
}
// applyEnvOverrides walks the Config struct and overrides fields from
// environment variables with the MCR_ prefix. For example,
// MCR_SERVER_LISTEN_ADDR overrides Config.Server.ListenAddr.
func applyEnvOverrides(cfg *Config) {
applyEnvToStruct(reflect.ValueOf(cfg).Elem(), "MCR")
}
func applyEnvToStruct(v reflect.Value, prefix string) {
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 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.Int64:
if field.Type == reflect.TypeOf(time.Duration(0)) {
d, err := time.ParseDuration(envVal)
if err == nil {
fv.Set(reflect.ValueOf(d))
}
}
case reflect.Slice:
if field.Type.Elem().Kind() == reflect.String {
parts := strings.Split(envVal, ",")
fv.Set(reflect.ValueOf(parts))
}
}
}
}