291 lines
8.2 KiB
Go
291 lines
8.2 KiB
Go
// Package bootstrap loads config and owns process lifecycle.
|
|
package bootstrap
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Env string `yaml:"env"`
|
|
Server ServerConfig `yaml:"server"`
|
|
Log LogConfig `yaml:"log"`
|
|
ID IDConfig `yaml:"id"`
|
|
MySQL MySQLConfig `yaml:"mysql"`
|
|
Redis RedisConfig `yaml:"redis"`
|
|
JWT JWTConfig `yaml:"jwt"`
|
|
TaskPool TaskPoolConfig `yaml:"taskpool"`
|
|
Deliver DeliverConfig `yaml:"deliver"`
|
|
Session SessionConfig `yaml:"session"`
|
|
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
|
Message MessageConfig `yaml:"message"`
|
|
ObjectStorage ObjectStorageConfig `yaml:"object_storage"`
|
|
}
|
|
|
|
type LogConfig struct {
|
|
Level string `yaml:"level"`
|
|
Encoding string `yaml:"encoding"`
|
|
Console bool `yaml:"console"`
|
|
File LogFileConfig `yaml:"file"`
|
|
}
|
|
|
|
type LogFileConfig struct {
|
|
Path string `yaml:"path"`
|
|
MaxSizeMB int `yaml:"max_size_mb"`
|
|
MaxBackups int `yaml:"max_backups"`
|
|
MaxAgeDays int `yaml:"max_age_days"`
|
|
Compress bool `yaml:"compress"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
HTTP HTTPConfig `yaml:"http"`
|
|
TCP TCPConfig `yaml:"tcp"`
|
|
}
|
|
|
|
type HTTPConfig struct {
|
|
Addr string `yaml:"addr"`
|
|
}
|
|
|
|
type TCPConfig struct {
|
|
Addr string `yaml:"addr"`
|
|
ProxyProtocol bool `yaml:"proxy_protocol"`
|
|
TLS TLSConfig `yaml:"tls"`
|
|
MaxFrameSize int `yaml:"max_frame_size"`
|
|
IdleTimeout time.Duration `yaml:"idle_timeout"`
|
|
KeepAlive time.Duration `yaml:"keepalive"`
|
|
RecvBuffer int `yaml:"recv_buffer"`
|
|
SendBuffer int `yaml:"send_buffer"`
|
|
Multicore bool `yaml:"multicore"`
|
|
CompressMinSize int `yaml:"compress_min_size"`
|
|
}
|
|
|
|
type TLSConfig struct {
|
|
CertFile string `yaml:"cert_file"`
|
|
KeyFile string `yaml:"key_file"`
|
|
}
|
|
|
|
type IDConfig struct {
|
|
SnowflakeWorkerID int64 `yaml:"snowflake_worker_id"`
|
|
}
|
|
|
|
type MySQLConfig struct {
|
|
DSN string `yaml:"dsn"`
|
|
SlowThreshold time.Duration `yaml:"slow_threshold"`
|
|
LogLevel string `yaml:"log_level"`
|
|
MaxOpenConns int `yaml:"max_open_conns"`
|
|
MaxIdleConns int `yaml:"max_idle_conns"`
|
|
ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime"`
|
|
ConnMaxIdleTime time.Duration `yaml:"conn_max_idle_time"`
|
|
}
|
|
|
|
type RedisConfig struct {
|
|
Addr string `yaml:"addr"`
|
|
Username string `yaml:"username"`
|
|
Password string `yaml:"password"`
|
|
DB int `yaml:"db"`
|
|
DeliverQueueKey string `yaml:"deliver_queue_key"`
|
|
DeliverProcessingKey string `yaml:"deliver_processing_key"`
|
|
DeliverDonePrefix string `yaml:"deliver_done_prefix"`
|
|
}
|
|
|
|
type TaskPoolConfig struct {
|
|
Size int `yaml:"size"`
|
|
Queue int `yaml:"queue"`
|
|
}
|
|
|
|
type DeliverConfig struct {
|
|
Workers int `yaml:"workers"`
|
|
TakeTimeout time.Duration `yaml:"take_timeout"`
|
|
DoneTTL time.Duration `yaml:"done_ttl"`
|
|
}
|
|
|
|
type SessionConfig struct {
|
|
OutboundQueueSize int `yaml:"outbound_queue_size"`
|
|
}
|
|
|
|
type RateLimitConfig struct {
|
|
MessagePerSecond int `yaml:"message_per_second"`
|
|
AuthPerSecond int `yaml:"auth_per_second"`
|
|
SyncInboxPerMinute int `yaml:"sync_inbox_per_minute"`
|
|
SyncMessagesPerMinute int `yaml:"sync_messages_per_minute"`
|
|
HistoryPerMinute int `yaml:"history_per_minute"`
|
|
UploadPerSecond int `yaml:"upload_per_second"`
|
|
}
|
|
|
|
type ObjectStorageConfig struct {
|
|
Endpoint string `yaml:"endpoint"`
|
|
PublicEndpoint string `yaml:"public_endpoint"`
|
|
AccessKey string `yaml:"access_key"`
|
|
SecretKey string `yaml:"secret_key"`
|
|
Bucket string `yaml:"bucket"`
|
|
Region string `yaml:"region"`
|
|
UseSSL bool `yaml:"use_ssl"`
|
|
PresignTTL time.Duration `yaml:"presign_ttl"`
|
|
ChatExpireDays int `yaml:"chat_expire_days"`
|
|
}
|
|
|
|
type MessageConfig struct {
|
|
MaxContentSize int `yaml:"max_content_size"`
|
|
SyncLimit int `yaml:"sync_limit"`
|
|
MsgIDExpireMS int64 `yaml:"msgid_expire_ms"`
|
|
}
|
|
|
|
type JWTConfig struct {
|
|
Secret string `yaml:"secret"`
|
|
Issuer string `yaml:"issuer"`
|
|
TTL time.Duration `yaml:"ttl"`
|
|
RefreshTTL time.Duration `yaml:"refresh_ttl"`
|
|
DenylistPrefix string `yaml:"denylist_prefix"`
|
|
RefreshPrefix string `yaml:"refresh_prefix"`
|
|
}
|
|
|
|
func Load(path string) (*Config, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config: %w", err)
|
|
}
|
|
cfg := &Config{}
|
|
if err := yaml.Unmarshal(raw, cfg); err != nil {
|
|
return nil, fmt.Errorf("parse config: %w", err)
|
|
}
|
|
if err := cfg.normalize(); err != nil {
|
|
return nil, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
const (
|
|
EnvDev = "dev"
|
|
EnvProd = "prod"
|
|
// DefaultJWTSecret is rejected when env is prod.
|
|
DefaultJWTSecret = "dev-only-change-me"
|
|
)
|
|
|
|
func (cfg *Config) Production() bool {
|
|
return cfg != nil && cfg.Env == EnvProd
|
|
}
|
|
|
|
func (cfg *Config) normalize() error {
|
|
if cfg == nil {
|
|
return fmt.Errorf("config is nil")
|
|
}
|
|
applyDefaults(cfg)
|
|
return cfg.validate()
|
|
}
|
|
|
|
func applyDefaults(cfg *Config) {
|
|
if v := os.Getenv("SPARK_ENV"); v != "" {
|
|
cfg.Env = v
|
|
}
|
|
cfg.Env = parseEnv(cfg.Env)
|
|
if v := os.Getenv("SPARK_JWT_SECRET"); v != "" {
|
|
cfg.JWT.Secret = v
|
|
}
|
|
if v := os.Getenv("SPARK_REDIS_PASSWORD"); v != "" {
|
|
cfg.Redis.Password = v
|
|
}
|
|
if cfg.JWT.Issuer == "" {
|
|
cfg.JWT.Issuer = "spark-im"
|
|
}
|
|
if cfg.JWT.TTL <= 0 {
|
|
cfg.JWT.TTL = 7 * 24 * time.Hour
|
|
}
|
|
if cfg.JWT.RefreshTTL <= 0 {
|
|
cfg.JWT.RefreshTTL = 30 * 24 * time.Hour
|
|
}
|
|
if cfg.JWT.DenylistPrefix == "" {
|
|
cfg.JWT.DenylistPrefix = "denylist:"
|
|
}
|
|
if cfg.JWT.RefreshPrefix == "" {
|
|
cfg.JWT.RefreshPrefix = "refresh:"
|
|
}
|
|
if cfg.Redis.DeliverQueueKey == "" {
|
|
cfg.Redis.DeliverQueueKey = "deliver:queue"
|
|
}
|
|
if cfg.Redis.DeliverProcessingKey == "" {
|
|
cfg.Redis.DeliverProcessingKey = "deliver:processing"
|
|
}
|
|
if cfg.Redis.DeliverDonePrefix == "" {
|
|
cfg.Redis.DeliverDonePrefix = "deliver:done:"
|
|
}
|
|
if cfg.Deliver.Workers < 1 {
|
|
cfg.Deliver.Workers = 8
|
|
}
|
|
if cfg.Deliver.TakeTimeout <= 0 {
|
|
cfg.Deliver.TakeTimeout = 5 * time.Second
|
|
}
|
|
if cfg.Deliver.DoneTTL <= 0 {
|
|
cfg.Deliver.DoneTTL = 10 * time.Minute
|
|
}
|
|
if cfg.RateLimit.AuthPerSecond < 1 {
|
|
cfg.RateLimit.AuthPerSecond = 5
|
|
}
|
|
if cfg.RateLimit.MessagePerSecond < 1 {
|
|
cfg.RateLimit.MessagePerSecond = 20
|
|
}
|
|
if v := os.Getenv("SPARK_S3_ENDPOINT"); v != "" {
|
|
cfg.ObjectStorage.Endpoint = v
|
|
}
|
|
if v := os.Getenv("SPARK_S3_PUBLIC_ENDPOINT"); v != "" {
|
|
cfg.ObjectStorage.PublicEndpoint = v
|
|
}
|
|
if v := os.Getenv("SPARK_S3_ACCESS_KEY"); v != "" {
|
|
cfg.ObjectStorage.AccessKey = v
|
|
}
|
|
if v := os.Getenv("SPARK_S3_SECRET_KEY"); v != "" {
|
|
cfg.ObjectStorage.SecretKey = v
|
|
}
|
|
if v := os.Getenv("SPARK_S3_BUCKET"); v != "" {
|
|
cfg.ObjectStorage.Bucket = v
|
|
}
|
|
if cfg.ObjectStorage.Bucket == "" {
|
|
cfg.ObjectStorage.Bucket = "spark"
|
|
}
|
|
if cfg.ObjectStorage.Region == "" {
|
|
cfg.ObjectStorage.Region = "us-east-1"
|
|
}
|
|
if cfg.ObjectStorage.PresignTTL <= 0 {
|
|
cfg.ObjectStorage.PresignTTL = 15 * time.Minute
|
|
}
|
|
if cfg.ObjectStorage.ChatExpireDays < 1 {
|
|
cfg.ObjectStorage.ChatExpireDays = 365
|
|
}
|
|
if cfg.RateLimit.UploadPerSecond < 1 {
|
|
cfg.RateLimit.UploadPerSecond = 10
|
|
}
|
|
}
|
|
|
|
func parseEnv(raw string) string {
|
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
|
case "prod", "production":
|
|
return EnvProd
|
|
default:
|
|
return EnvDev
|
|
}
|
|
}
|
|
|
|
func (cfg *Config) validate() error {
|
|
if !cfg.Production() {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(cfg.MySQL.DSN) == "" {
|
|
return fmt.Errorf("prod: mysql.dsn is required")
|
|
}
|
|
if strings.TrimSpace(cfg.Redis.Addr) == "" {
|
|
return fmt.Errorf("prod: redis.addr is required")
|
|
}
|
|
if strings.TrimSpace(cfg.JWT.Secret) == "" || strings.TrimSpace(cfg.JWT.Secret) == DefaultJWTSecret {
|
|
return fmt.Errorf("prod: jwt.secret must be set via config or SPARK_JWT_SECRET")
|
|
}
|
|
if ep := strings.TrimSpace(cfg.ObjectStorage.Endpoint); ep != "" {
|
|
if strings.TrimSpace(cfg.ObjectStorage.AccessKey) == "" || strings.TrimSpace(cfg.ObjectStorage.SecretKey) == "" {
|
|
return fmt.Errorf("prod: object_storage access_key and secret_key are required")
|
|
}
|
|
}
|
|
return nil
|
|
}
|