CI / Test (linux/amd64) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Cross-build (darwin/amd64) (push) Waiting to run
CI / Cross-build (linux/amd64) (push) Waiting to run
CI / Cross-build (darwin/arm64) (push) Waiting to run
CI / Cross-build (linux/arm64) (push) Waiting to run
- Migrate .golangci.yml to golangci-lint v2 config format - Bump golangci-lint-action from v6 (Node 20, deprecated) to v9 (Node 24) - Fix errcheck: check fmt.Fscanln and fmt.Sscanf return values - Fix ineffassign: replace unused vars with _ in test files - Fix revive unused-parameter: rename unused cmd/args/r/c params to _ - Fix revive empty-block: invert if condition in push command - Fix revive redefines-builtin-id: rename 'clear' to 'clearsigned' - Fix revive blank-imports: add comment on modernc.org/sqlite import - Fix unused: remove unused 'suite' and 'gen' fields from cache entry
222 lines
5.8 KiB
Go
222 lines
5.8 KiB
Go
// Package config defines the urapt-server configuration and its loading from
|
|
// defaults, a TOML file, environment variables, and command-line flags, with
|
|
// later sources overriding earlier ones.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
// Defaults applied before any other source.
|
|
var Defaults = Config{
|
|
Bind: "0.0.0.0:8080",
|
|
BaseURL: "http://localhost:8080",
|
|
StoreDir: "./store",
|
|
LogLevel: "info",
|
|
SigningKeyType: "rsa",
|
|
SigningKeyBits: 4096,
|
|
MaxPackageSize: 1024 * 1024 * 1024,
|
|
OpenRegistration: true,
|
|
ConfigPath: "./urapt-server.toml",
|
|
}
|
|
|
|
// Config is the urapt-server runtime configuration.
|
|
type Config struct {
|
|
Bind string `toml:"bind"`
|
|
BaseURL string `toml:"base_url"`
|
|
StoreDir string `toml:"store_dir"`
|
|
DBPath string `toml:"db_path"`
|
|
PackagesDir string `toml:"packages_dir"`
|
|
LogLevel string `toml:"log_level"`
|
|
SigningKeyType string `toml:"signing_key_type"`
|
|
SigningKeyBits int `toml:"signing_key_bits"`
|
|
SigningKeyUserID string `toml:"signing_key_user_id"`
|
|
MaxPackageSize int64 `toml:"max_package_size"`
|
|
OpenRegistration bool `toml:"open_registration"`
|
|
TLSEnabled bool `toml:"tls_enabled"`
|
|
TLSCert string `toml:"tls_cert"`
|
|
TLSKey string `toml:"tls_key"`
|
|
|
|
ConfigPath string `toml:"-"`
|
|
}
|
|
|
|
// Load builds the effective Config from Defaults -> file -> env -> flags.
|
|
// Flags is a map of flag name to string value (already parsed by the caller).
|
|
func Load(configPath string, flags map[string]string) (Config, error) {
|
|
c := Defaults
|
|
c.ConfigPath = configPath
|
|
|
|
if err := applyFile(&c, configPath); err != nil {
|
|
return Config{}, err
|
|
}
|
|
applyEnv(&c)
|
|
if err := applyFlags(&c, flags); err != nil {
|
|
return Config{}, err
|
|
}
|
|
c.finalize()
|
|
return c, nil
|
|
}
|
|
|
|
func applyFile(c *Config, path string) error {
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("read config %s: %w", path, err)
|
|
}
|
|
if err := toml.Unmarshal(data, c); err != nil {
|
|
return fmt.Errorf("parse config %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyEnv(c *Config) {
|
|
set(c, "URAPT_BIND", &c.Bind)
|
|
set(c, "URAPT_BASE_URL", &c.BaseURL)
|
|
set(c, "URAPT_STORE_DIR", &c.StoreDir)
|
|
set(c, "URAPT_DB_PATH", &c.DBPath)
|
|
set(c, "URAPT_PACKAGES_DIR", &c.PackagesDir)
|
|
set(c, "URAPT_LOG_LEVEL", &c.LogLevel)
|
|
set(c, "URAPT_SIGNING_KEY_TYPE", &c.SigningKeyType)
|
|
set(c, "URAPT_SIGNING_KEY_USER_ID", &c.SigningKeyUserID)
|
|
set(c, "URAPT_TLS_CERT", &c.TLSCert)
|
|
set(c, "URAPT_TLS_KEY", &c.TLSKey)
|
|
setInt(c, "URAPT_SIGNING_KEY_BITS", &c.SigningKeyBits)
|
|
setInt64(c, "URAPT_MAX_PACKAGE_SIZE", &c.MaxPackageSize)
|
|
setBool(c, "URAPT_OPEN_REGISTRATION", &c.OpenRegistration)
|
|
setBool(c, "URAPT_TLS_ENABLED", &c.TLSEnabled)
|
|
}
|
|
|
|
func applyFlags(c *Config, flags map[string]string) error {
|
|
for k, v := range flags {
|
|
switch k {
|
|
case "bind":
|
|
c.Bind = v
|
|
case "base-url":
|
|
c.BaseURL = v
|
|
case "store-dir":
|
|
c.StoreDir = v
|
|
case "db-path":
|
|
c.DBPath = v
|
|
case "packages-dir":
|
|
c.PackagesDir = v
|
|
case "log-level":
|
|
c.LogLevel = v
|
|
case "signing-key-type":
|
|
c.SigningKeyType = v
|
|
case "signing-key-bits":
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid --signing-key-bits %q: %w", v, err)
|
|
}
|
|
c.SigningKeyBits = n
|
|
case "signing-key-user-id":
|
|
c.SigningKeyUserID = v
|
|
case "max-package-size":
|
|
n, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid --max-package-size %q: %w", v, err)
|
|
}
|
|
c.MaxPackageSize = n
|
|
case "open-registration":
|
|
b, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid --open-registration %q: %w", v, err)
|
|
}
|
|
c.OpenRegistration = b
|
|
case "tls-enabled":
|
|
b, err := strconv.ParseBool(v)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid --tls-enabled %q: %w", v, err)
|
|
}
|
|
c.TLSEnabled = b
|
|
case "tls-cert":
|
|
c.TLSCert = v
|
|
case "tls-key":
|
|
c.TLSKey = v
|
|
case "config":
|
|
// already handled
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// finalize fills derived defaults: DBPath and PackagesDir default under
|
|
// StoreDir, and SigningKeyUserID gets a hostname-based default.
|
|
func (c *Config) finalize() {
|
|
if c.DBPath == "" {
|
|
c.DBPath = filepath.Join(c.StoreDir, "database", "sqlite.db")
|
|
}
|
|
if c.PackagesDir == "" {
|
|
c.PackagesDir = filepath.Join(c.StoreDir, "packages")
|
|
}
|
|
if c.SigningKeyUserID == "" {
|
|
host, err := os.Hostname()
|
|
if err != nil || host == "" {
|
|
host = "localhost"
|
|
}
|
|
c.SigningKeyUserID = "urapt-server <" + host + ">"
|
|
}
|
|
if c.MaxPackageSize <= 0 {
|
|
c.MaxPackageSize = Defaults.MaxPackageSize
|
|
}
|
|
}
|
|
|
|
// Validate checks the config for obvious errors before startup.
|
|
func (c *Config) Validate() error {
|
|
if c.Bind == "" {
|
|
return fmt.Errorf("bind address is required")
|
|
}
|
|
if c.BaseURL == "" {
|
|
return fmt.Errorf("base_url is required")
|
|
}
|
|
c.BaseURL = strings.TrimRight(c.BaseURL, "/")
|
|
if c.SigningKeyBits <= 0 {
|
|
return fmt.Errorf("signing_key_bits must be positive")
|
|
}
|
|
if c.TLSEnabled && (c.TLSCert == "" || c.TLSKey == "") {
|
|
return fmt.Errorf("tls_enabled requires tls_cert and tls_key")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func set(_ *Config, env string, dst *string) {
|
|
if v, ok := os.LookupEnv(env); ok && v != "" {
|
|
*dst = v
|
|
}
|
|
}
|
|
|
|
func setInt(_ *Config, env string, dst *int) {
|
|
if v, ok := os.LookupEnv(env); ok && v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
*dst = n
|
|
}
|
|
}
|
|
}
|
|
|
|
func setInt64(_ *Config, env string, dst *int64) {
|
|
if v, ok := os.LookupEnv(env); ok && v != "" {
|
|
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
|
*dst = n
|
|
}
|
|
}
|
|
}
|
|
|
|
func setBool(_ *Config, env string, dst *bool) {
|
|
if v, ok := os.LookupEnv(env); ok && v != "" {
|
|
if b, err := strconv.ParseBool(v); err == nil {
|
|
*dst = b
|
|
}
|
|
}
|
|
}
|