This repository has been archived on 2026-07-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
codespace/pkg/config/config.go
T
2026-07-02 17:22:26 +08:00

203 lines
5.4 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"`
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"`
}
// 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"`
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},
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
}
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.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_LOG_LEVEL"); v != "" {
cfg.Log.Level = v
}
if v := os.Getenv("CODESPACE_LOG_FORMAT"); v != "" {
cfg.Log.Format = v
}
return nil
}