229 lines
6.0 KiB
Go
229 lines
6.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config holds all runtime configuration for the codespace server.
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
Workspace WorkspaceConfig `yaml:"workspace"`
|
|
Process ProcessConfig `yaml:"process"`
|
|
File FileConfig `yaml:"file"`
|
|
Gin GinConfig `yaml:"gin"`
|
|
Log LogConfig `yaml:"log"`
|
|
}
|
|
|
|
// ServerConfig holds HTTP server settings.
|
|
type ServerConfig struct {
|
|
Addr string
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
IdleTimeout time.Duration
|
|
MaxHeaderBytes int
|
|
}
|
|
|
|
// WorkspaceConfig holds workspace directory settings.
|
|
type WorkspaceConfig struct {
|
|
Root string `yaml:"root"`
|
|
}
|
|
|
|
// ProcessConfig holds OpenCode process settings.
|
|
type ProcessConfig struct {
|
|
OpenCodeCommand string `yaml:"opencodeCommand"`
|
|
}
|
|
|
|
// FileConfig holds file operation limits.
|
|
type FileConfig struct {
|
|
MaxWriteBytes int64 `yaml:"maxWriteBytes"`
|
|
}
|
|
|
|
// GinConfig holds Gin framework runtime settings.
|
|
type GinConfig struct {
|
|
Mode string `yaml:"mode"`
|
|
}
|
|
|
|
// LogConfig holds structured logging settings.
|
|
type LogConfig struct {
|
|
Level string `yaml:"level"`
|
|
Format string `yaml:"format"`
|
|
}
|
|
|
|
// rawConfig is used for YAML deserialization so that duration strings can be
|
|
// parsed explicitly rather than relying on implicit time.Duration unmarshalling.
|
|
type rawConfig struct {
|
|
Server rawServerConfig `yaml:"server"`
|
|
Workspace WorkspaceConfig `yaml:"workspace"`
|
|
Process ProcessConfig `yaml:"process"`
|
|
File FileConfig `yaml:"file"`
|
|
Gin GinConfig `yaml:"gin"`
|
|
Log LogConfig `yaml:"log"`
|
|
}
|
|
|
|
type rawServerConfig struct {
|
|
Addr string `yaml:"addr"`
|
|
ReadTimeout string `yaml:"readTimeout"`
|
|
WriteTimeout string `yaml:"writeTimeout"`
|
|
IdleTimeout string `yaml:"idleTimeout"`
|
|
MaxHeaderBytes int `yaml:"maxHeaderBytes"`
|
|
}
|
|
|
|
// Default returns the built-in default configuration.
|
|
func Default() Config {
|
|
return Config{
|
|
Server: ServerConfig{
|
|
Addr: ":8080",
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
MaxHeaderBytes: 1 << 20, // 1 MiB
|
|
},
|
|
Workspace: WorkspaceConfig{Root: "./workspaces"},
|
|
Process: ProcessConfig{OpenCodeCommand: "opencode"},
|
|
File: FileConfig{MaxWriteBytes: 1 << 20},
|
|
Gin: GinConfig{Mode: "release"},
|
|
Log: LogConfig{Level: "info", Format: "json"},
|
|
}
|
|
}
|
|
|
|
// Load reads configuration from the given YAML file path (if it exists),
|
|
// then applies environment variable overrides. Missing files are not an error.
|
|
func Load(path string) (Config, error) {
|
|
cfg := Default()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err == nil {
|
|
var raw rawConfig
|
|
if err := yaml.Unmarshal(data, &raw); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := applyRaw(&cfg, raw); err != nil {
|
|
return Config{}, err
|
|
}
|
|
} else if !os.IsNotExist(err) {
|
|
return Config{}, err
|
|
}
|
|
|
|
if err := applyEnv(&cfg); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := validateGinMode(cfg.Gin.Mode); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func applyRaw(cfg *Config, raw rawConfig) error {
|
|
if raw.Server.Addr != "" {
|
|
cfg.Server.Addr = raw.Server.Addr
|
|
}
|
|
if err := applyDuration(&cfg.Server.ReadTimeout, raw.Server.ReadTimeout, "readTimeout"); err != nil {
|
|
return err
|
|
}
|
|
if err := applyDuration(&cfg.Server.WriteTimeout, raw.Server.WriteTimeout, "writeTimeout"); err != nil {
|
|
return err
|
|
}
|
|
if err := applyDuration(&cfg.Server.IdleTimeout, raw.Server.IdleTimeout, "idleTimeout"); err != nil {
|
|
return err
|
|
}
|
|
if raw.Server.MaxHeaderBytes != 0 {
|
|
cfg.Server.MaxHeaderBytes = raw.Server.MaxHeaderBytes
|
|
}
|
|
if raw.Workspace.Root != "" {
|
|
cfg.Workspace.Root = raw.Workspace.Root
|
|
}
|
|
if raw.Process.OpenCodeCommand != "" {
|
|
cfg.Process.OpenCodeCommand = raw.Process.OpenCodeCommand
|
|
}
|
|
if raw.File.MaxWriteBytes != 0 {
|
|
cfg.File.MaxWriteBytes = raw.File.MaxWriteBytes
|
|
}
|
|
if raw.Gin.Mode != "" {
|
|
cfg.Gin.Mode = raw.Gin.Mode
|
|
}
|
|
if raw.Log.Level != "" {
|
|
cfg.Log.Level = raw.Log.Level
|
|
}
|
|
if raw.Log.Format != "" {
|
|
cfg.Log.Format = raw.Log.Format
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyDuration(target *time.Duration, value, name string) error {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
d, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid duration for %s: %w", name, err)
|
|
}
|
|
*target = d
|
|
return nil
|
|
}
|
|
|
|
func applyEnv(cfg *Config) error {
|
|
if v := os.Getenv("CODESPACE_ADDR"); v != "" {
|
|
cfg.Server.Addr = v
|
|
}
|
|
if v := os.Getenv("CODESPACE_READ_TIMEOUT"); v != "" {
|
|
if err := applyDuration(&cfg.Server.ReadTimeout, v, "CODESPACE_READ_TIMEOUT"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := os.Getenv("CODESPACE_WRITE_TIMEOUT"); v != "" {
|
|
if err := applyDuration(&cfg.Server.WriteTimeout, v, "CODESPACE_WRITE_TIMEOUT"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := os.Getenv("CODESPACE_IDLE_TIMEOUT"); v != "" {
|
|
if err := applyDuration(&cfg.Server.IdleTimeout, v, "CODESPACE_IDLE_TIMEOUT"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := os.Getenv("CODESPACE_MAX_HEADER_BYTES"); v != "" {
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid integer for CODESPACE_MAX_HEADER_BYTES: %w", err)
|
|
}
|
|
cfg.Server.MaxHeaderBytes = n
|
|
}
|
|
if v := os.Getenv("CODESPACE_WORKSPACE_ROOT"); v != "" {
|
|
cfg.Workspace.Root = v
|
|
}
|
|
if v := os.Getenv("CODESPACE_OPENCODE_COMMAND"); v != "" {
|
|
cfg.Process.OpenCodeCommand = v
|
|
}
|
|
if v := os.Getenv("CODESPACE_FILE_MAX_WRITE_BYTES"); v != "" {
|
|
n, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid integer for CODESPACE_FILE_MAX_WRITE_BYTES: %w", err)
|
|
}
|
|
cfg.File.MaxWriteBytes = n
|
|
}
|
|
if v := os.Getenv("CODESPACE_GIN_MODE"); v != "" {
|
|
cfg.Gin.Mode = v
|
|
}
|
|
if v := os.Getenv("CODESPACE_LOG_LEVEL"); v != "" {
|
|
cfg.Log.Level = v
|
|
}
|
|
if v := os.Getenv("CODESPACE_LOG_FORMAT"); v != "" {
|
|
cfg.Log.Format = v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateGinMode(mode string) error {
|
|
switch mode {
|
|
case "debug", "release", "test":
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("invalid gin mode %q: must be one of debug, release, test", mode)
|
|
}
|
|
}
|