diff --git a/internal/config/config.example.yaml b/internal/config/config.example.yaml new file mode 100644 index 0000000..c8283a4 --- /dev/null +++ b/internal/config/config.example.yaml @@ -0,0 +1,16 @@ +server_port: "8080" +slurm_api_url: "http://localhost:6820" +slurm_user_name: "root" +slurm_jwt_key_path: "/etc/slurm/jwt_hs256.key" +mysql_dsn: "root:@tcp(127.0.0.1:3306)/hpc_platform?parseTime=true" + +log: + level: "info" # debug, info, warn, error + encoding: "json" # json, console + output_stdout: true # 是否输出日志到终端 + file_path: "" # 日志文件路径,留空则不写文件 + max_size: 100 # max MB per log file + max_backups: 5 # number of old log files to retain + max_age: 30 # days to retain old log files + compress: true # gzip rotated log files + gorm_level: "warn" # GORM SQL log level: silent, error, warn, info diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..c5e66fa --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,51 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// LogConfig holds logging configuration values. +type LogConfig struct { + Level string `yaml:"level"` // debug, info, warn, error (default: info) + Encoding string `yaml:"encoding"` // json, console (default: json) + OutputStdout *bool `yaml:"output_stdout"` // 输出到终端 (default: true) + FilePath string `yaml:"file_path"` // log file path (for rotation) + MaxSize int `yaml:"max_size"` // MB per file (default: 100) + MaxBackups int `yaml:"max_backups"` // retained files (default: 5) + MaxAge int `yaml:"max_age"` // days to retain (default: 30) + Compress bool `yaml:"compress"` // gzip old files (default: true) + GormLevel string `yaml:"gorm_level"` // GORM SQL log level (default: warn) +} + +// Config holds all application configuration values. +type Config struct { + ServerPort string `yaml:"server_port"` + SlurmAPIURL string `yaml:"slurm_api_url"` + SlurmUserName string `yaml:"slurm_user_name"` + SlurmJWTKeyPath string `yaml:"slurm_jwt_key_path"` + MySQLDSN string `yaml:"mysql_dsn"` + Log LogConfig `yaml:"log"` +} + +// Load reads a YAML configuration file and returns a parsed Config. +// If path is empty, it defaults to "./config.yaml". +func Load(path string) (*Config, error) { + if path == "" { + path = "./config.yaml" + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read config file %s: %w", path, err) + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config file %s: %w", path, err) + } + + return &cfg, nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..38af357 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,256 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoad(t *testing.T) { + content := []byte(`server_port: "9090" +slurm_api_url: "http://slurm.example.com:6820" +slurm_user_name: "admin" +slurm_jwt_key_path: "/etc/slurm/jwt.key" +mysql_dsn: "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true" +`) + + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("write temp config: %v", err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.ServerPort != "9090" { + t.Errorf("ServerPort = %q, want %q", cfg.ServerPort, "9090") + } + if cfg.SlurmAPIURL != "http://slurm.example.com:6820" { + t.Errorf("SlurmAPIURL = %q, want %q", cfg.SlurmAPIURL, "http://slurm.example.com:6820") + } + if cfg.SlurmUserName != "admin" { + t.Errorf("SlurmUserName = %q, want %q", cfg.SlurmUserName, "admin") + } + if cfg.SlurmJWTKeyPath != "/etc/slurm/jwt.key" { + t.Errorf("SlurmJWTKeyPath = %q, want %q", cfg.SlurmJWTKeyPath, "/etc/slurm/jwt.key") + } + if cfg.MySQLDSN != "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true" { + t.Errorf("MySQLDSN = %q, want %q", cfg.MySQLDSN, "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true") + } +} + +func TestLoadDefaultPath(t *testing.T) { + content := []byte(`server_port: "8080" +slurm_api_url: "http://localhost:6820" +slurm_user_name: "root" +slurm_jwt_key_path: "/etc/slurm/jwt_hs256.key" +mysql_dsn: "root:@tcp(127.0.0.1:3306)/hpc_platform?parseTime=true" +`) + + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("write temp config: %v", err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if cfg == nil { + t.Fatal("Load() returned nil config") + } +} + +func TestLoadWithLogConfig(t *testing.T) { + content := []byte(`server_port: "9090" +slurm_api_url: "http://slurm.example.com:6820" +slurm_user_name: "admin" +slurm_jwt_key_path: "/etc/slurm/jwt.key" +mysql_dsn: "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true" +log: + level: "debug" + encoding: "console" + output_stdout: true + file_path: "/var/log/app.log" + max_size: 200 + max_backups: 10 + max_age: 60 + compress: false + gorm_level: "info" +`) + + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("write temp config: %v", err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.Log.Level != "debug" { + t.Errorf("Log.Level = %q, want %q", cfg.Log.Level, "debug") + } + if cfg.Log.Encoding != "console" { + t.Errorf("Log.Encoding = %q, want %q", cfg.Log.Encoding, "console") + } + if cfg.Log.OutputStdout == nil || *cfg.Log.OutputStdout != true { + t.Errorf("Log.OutputStdout = %v, want true", cfg.Log.OutputStdout) + } + if cfg.Log.FilePath != "/var/log/app.log" { + t.Errorf("Log.FilePath = %q, want %q", cfg.Log.FilePath, "/var/log/app.log") + } + if cfg.Log.MaxSize != 200 { + t.Errorf("Log.MaxSize = %d, want %d", cfg.Log.MaxSize, 200) + } + if cfg.Log.MaxBackups != 10 { + t.Errorf("Log.MaxBackups = %d, want %d", cfg.Log.MaxBackups, 10) + } + if cfg.Log.MaxAge != 60 { + t.Errorf("Log.MaxAge = %d, want %d", cfg.Log.MaxAge, 60) + } + if cfg.Log.Compress != false { + t.Errorf("Log.Compress = %v, want %v", cfg.Log.Compress, false) + } + if cfg.Log.GormLevel != "info" { + t.Errorf("Log.GormLevel = %q, want %q", cfg.Log.GormLevel, "info") + } +} + +func TestLoadWithoutLogConfig(t *testing.T) { + content := []byte(`server_port: "8080" +slurm_api_url: "http://localhost:6820" +slurm_user_name: "root" +slurm_jwt_key_path: "/etc/slurm/jwt_hs256.key" +mysql_dsn: "root:@tcp(127.0.0.1:3306)/hpc_platform?parseTime=true" +`) + + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("write temp config: %v", err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.Log.Level != "" { + t.Errorf("Log.Level = %q, want empty string", cfg.Log.Level) + } + if cfg.Log.Encoding != "" { + t.Errorf("Log.Encoding = %q, want empty string", cfg.Log.Encoding) + } + if cfg.Log.OutputStdout != nil { + t.Errorf("Log.OutputStdout = %v, want nil", cfg.Log.OutputStdout) + } + if cfg.Log.FilePath != "" { + t.Errorf("Log.FilePath = %q, want empty string", cfg.Log.FilePath) + } + if cfg.Log.MaxSize != 0 { + t.Errorf("Log.MaxSize = %d, want 0", cfg.Log.MaxSize) + } + if cfg.Log.MaxBackups != 0 { + t.Errorf("Log.MaxBackups = %d, want 0", cfg.Log.MaxBackups) + } + if cfg.Log.MaxAge != 0 { + t.Errorf("Log.MaxAge = %d, want 0", cfg.Log.MaxAge) + } + if cfg.Log.Compress != false { + t.Errorf("Log.Compress = %v, want false", cfg.Log.Compress) + } + if cfg.Log.GormLevel != "" { + t.Errorf("Log.GormLevel = %q, want empty string", cfg.Log.GormLevel) + } +} + +func TestLoadExistingFieldsWithLogConfig(t *testing.T) { + content := []byte(`server_port: "7070" +slurm_api_url: "http://slurm2.example.com:6820" +slurm_user_name: "testuser" +slurm_jwt_key_path: "/keys/jwt.key" +mysql_dsn: "root:secret@tcp(db:3306)/mydb?parseTime=true" +log: + level: "warn" + encoding: "json" +`) + + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("write temp config: %v", err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.ServerPort != "7070" { + t.Errorf("ServerPort = %q, want %q", cfg.ServerPort, "7070") + } + if cfg.SlurmAPIURL != "http://slurm2.example.com:6820" { + t.Errorf("SlurmAPIURL = %q, want %q", cfg.SlurmAPIURL, "http://slurm2.example.com:6820") + } + if cfg.SlurmUserName != "testuser" { + t.Errorf("SlurmUserName = %q, want %q", cfg.SlurmUserName, "testuser") + } + if cfg.SlurmJWTKeyPath != "/keys/jwt.key" { + t.Errorf("SlurmJWTKeyPath = %q, want %q", cfg.SlurmJWTKeyPath, "/keys/jwt.key") + } + if cfg.MySQLDSN != "root:secret@tcp(db:3306)/mydb?parseTime=true" { + t.Errorf("MySQLDSN = %q, want %q", cfg.MySQLDSN, "root:secret@tcp(db:3306)/mydb?parseTime=true") + } + if cfg.Log.Level != "warn" { + t.Errorf("Log.Level = %q, want %q", cfg.Log.Level, "warn") + } + if cfg.Log.Encoding != "json" { + t.Errorf("Log.Encoding = %q, want %q", cfg.Log.Encoding, "json") + } +} + +func TestLoadNonExistentFile(t *testing.T) { + _, err := Load("/nonexistent/path/config.yaml") + if err == nil { + t.Fatal("Load() expected error for non-existent file, got nil") + } +} + +func TestLoadWithOutputStdoutFalse(t *testing.T) { + content := []byte(`server_port: "9090" +slurm_api_url: "http://slurm.example.com:6820" +slurm_user_name: "admin" +slurm_jwt_key_path: "/etc/slurm/jwt.key" +mysql_dsn: "user:pass@tcp(10.0.0.1:3306)/testdb?parseTime=true" +log: + level: "info" + encoding: "json" + output_stdout: false + file_path: "/var/log/app.log" +`) + + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("write temp config: %v", err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.Log.OutputStdout == nil || *cfg.Log.OutputStdout != false { + t.Errorf("Log.OutputStdout = %v, want false", cfg.Log.OutputStdout) + } + if cfg.Log.FilePath != "/var/log/app.log" { + t.Errorf("Log.FilePath = %q, want %q", cfg.Log.FilePath, "/var/log/app.log") + } +}