DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,951 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
const defaultWSPayloadMax = 1024 * 1024
|
||||
const defaultWSHBSeconds = 10
|
||||
const defaultRPCPayloadMax = 1024 * 1024
|
||||
const defaultKCPWindowSize = 128
|
||||
const defaultQUICStreams int64 = 128
|
||||
const defRateWindow = 10
|
||||
const defaultConnRateMax = 60
|
||||
const defConnMaxEntries = 4096
|
||||
const defConnIPv4Prefix = 32
|
||||
const defConnIPv6Prefix = 64
|
||||
const defaultEventLogTable = "outbox_events"
|
||||
const defaultAccountTable = "account_center_state"
|
||||
const defaultObjectTable = "storage_objects"
|
||||
const defaultOutboxStream = "battle.settlement"
|
||||
const defaultOutboxType = "battle.settlement.created"
|
||||
|
||||
func Load(path, defaultName string) (ServiceConfig, error) {
|
||||
var cfg ServiceConfig
|
||||
meta, err := toml.DecodeFile(path, &cfg)
|
||||
if err != nil {
|
||||
return ServiceConfig{}, err
|
||||
}
|
||||
if keys := undecodedTOMLKeys(meta); len(keys) > 0 {
|
||||
return ServiceConfig{}, fmt.Errorf("unknown config keys: %s", strings.Join(keys, ", "))
|
||||
}
|
||||
expandEnvValues(&cfg)
|
||||
cfg.Normalize(defaultName)
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return ServiceConfig{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func undecodedTOMLKeys(meta toml.MetaData) []string {
|
||||
undecoded := meta.Undecoded()
|
||||
if len(undecoded) == 0 {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(undecoded))
|
||||
for _, key := range undecoded {
|
||||
keys = append(keys, key.String())
|
||||
}
|
||||
slices.Sort(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) Normalize(defaultName string) {
|
||||
c.trim()
|
||||
if c.Service.Name == "" {
|
||||
c.Service.Name = defaultName
|
||||
}
|
||||
if c.Service.NodeID == "" {
|
||||
c.Service.NodeID = c.Service.Name + "-local-1"
|
||||
}
|
||||
if c.Service.Environment == "" {
|
||||
c.Service.Environment = "local"
|
||||
}
|
||||
if c.Service.StopTimeoutSeconds == 0 {
|
||||
c.Service.StopTimeoutSeconds = 10
|
||||
}
|
||||
if c.Service.StopInitialGraceSeconds == 0 {
|
||||
c.Service.StopInitialGraceSeconds = 2
|
||||
}
|
||||
if c.Service.StopProbeSeconds == 0 {
|
||||
c.Service.StopProbeSeconds = 1
|
||||
}
|
||||
if c.Service.StopStableSeconds == 0 {
|
||||
c.Service.StopStableSeconds = 5
|
||||
}
|
||||
if c.Cluster.ID == "" {
|
||||
c.Cluster.ID = "local"
|
||||
}
|
||||
if c.Cluster.VirtualGID == 0 {
|
||||
c.Cluster.VirtualGID = c.Cluster.GID
|
||||
}
|
||||
if c.Cluster.Zone == "" {
|
||||
c.Cluster.Zone = c.Service.Environment
|
||||
}
|
||||
if c.Cluster.Version == "" {
|
||||
c.Cluster.Version = "local"
|
||||
}
|
||||
if c.Cluster.DataVersion == "" {
|
||||
c.Cluster.DataVersion = "local"
|
||||
}
|
||||
if c.Cluster.HeartbeatIntervalSeconds == 0 {
|
||||
c.Cluster.HeartbeatIntervalSeconds = 10
|
||||
}
|
||||
if c.Admin.Listen == "" {
|
||||
c.Admin.Listen = ":18080"
|
||||
}
|
||||
for idx := range c.Admin.Tokens {
|
||||
if c.Admin.Tokens[idx].Role == "" {
|
||||
c.Admin.Tokens[idx].Role = "operator"
|
||||
}
|
||||
}
|
||||
if c.Admin.ReadTimeoutSeconds == 0 {
|
||||
c.Admin.ReadTimeoutSeconds = 10
|
||||
}
|
||||
if c.Admin.ReadHeaderTimeoutSeconds == 0 {
|
||||
c.Admin.ReadHeaderTimeoutSeconds = 5
|
||||
}
|
||||
if c.Admin.WriteTimeoutSeconds == 0 {
|
||||
c.Admin.WriteTimeoutSeconds = 15
|
||||
}
|
||||
if c.Admin.IdleTimeoutSeconds == 0 {
|
||||
c.Admin.IdleTimeoutSeconds = 60
|
||||
}
|
||||
if c.RateLimit.Algorithm == "" {
|
||||
c.RateLimit.Algorithm = "window"
|
||||
}
|
||||
if c.RateLimit.WindowSeconds == 0 {
|
||||
c.RateLimit.WindowSeconds = 1
|
||||
}
|
||||
if c.RateLimit.MaxRequests == 0 {
|
||||
c.RateLimit.MaxRequests = 60
|
||||
}
|
||||
if c.RateLimit.CleanupIntervalSeconds == 0 {
|
||||
c.RateLimit.CleanupIntervalSeconds = 60
|
||||
}
|
||||
if c.Tracing.MaxSpans == 0 {
|
||||
c.Tracing.MaxSpans = 256
|
||||
}
|
||||
if c.OTel.Endpoint == "" {
|
||||
c.OTel.Endpoint = "127.0.0.1:4318"
|
||||
}
|
||||
if c.OTel.ServiceName == "" {
|
||||
c.OTel.ServiceName = c.Service.Name
|
||||
}
|
||||
if c.OTel.SampleRatio == 0 {
|
||||
c.OTel.SampleRatio = 1
|
||||
}
|
||||
if c.OTel.BatchTimeoutSeconds == 0 {
|
||||
c.OTel.BatchTimeoutSeconds = 5
|
||||
}
|
||||
if c.DataTables.Directory == "" {
|
||||
c.DataTables.Directory = "configs/tables"
|
||||
}
|
||||
if c.DataTables.Version == "" {
|
||||
c.DataTables.Version = c.Cluster.DataVersion
|
||||
}
|
||||
if c.I18N.Directory == "" {
|
||||
c.I18N.Directory = "configs/i18n"
|
||||
}
|
||||
if c.I18N.DefaultLanguage == "" {
|
||||
c.I18N.DefaultLanguage = "zh-CN"
|
||||
}
|
||||
if c.I18N.Version == "" {
|
||||
c.I18N.Version = c.Cluster.DataVersion
|
||||
}
|
||||
c.normalizePVF()
|
||||
if c.Audit.Kind == "" {
|
||||
c.Audit.Kind = "memory"
|
||||
}
|
||||
if c.Audit.FilePath == "" {
|
||||
c.Audit.FilePath = "data/audit/events.jsonl"
|
||||
}
|
||||
if c.Audit.MemoryLimit == 0 {
|
||||
c.Audit.MemoryLimit = 256
|
||||
}
|
||||
if c.LogicLog.Kind == "" {
|
||||
c.LogicLog.Kind = "memory"
|
||||
}
|
||||
if c.LogicLog.FilePath == "" {
|
||||
c.LogicLog.FilePath = "data/logiclog/events.jsonl"
|
||||
}
|
||||
if c.LogicLog.ReasonsPath == "" {
|
||||
c.LogicLog.ReasonsPath = "configs/logiclog/reasons.csv"
|
||||
}
|
||||
if c.LogicLog.MemoryLimit == 0 {
|
||||
c.LogicLog.MemoryLimit = 256
|
||||
}
|
||||
if c.BILog.Kind == "" {
|
||||
c.BILog.Kind = "memory"
|
||||
}
|
||||
if c.BILog.FilePath == "" {
|
||||
c.BILog.FilePath = "data/bilog/events.jsonl"
|
||||
}
|
||||
if c.BILog.SchemaPath == "" {
|
||||
c.BILog.SchemaPath = "configs/bilog/schema.json"
|
||||
}
|
||||
if c.BILog.MemoryLimit == 0 {
|
||||
c.BILog.MemoryLimit = 256
|
||||
}
|
||||
if c.Bus.NATSName == "" {
|
||||
c.Bus.NATSName = "longheng-bus"
|
||||
}
|
||||
if c.Bus.NATSTimeoutSeconds == 0 {
|
||||
c.Bus.NATSTimeoutSeconds = 5
|
||||
}
|
||||
if c.Bus.NATSPingIntervalSeconds == 0 {
|
||||
c.Bus.NATSPingIntervalSeconds = 1
|
||||
}
|
||||
if c.Bus.NATSMaxPingsOutstanding == 0 {
|
||||
c.Bus.NATSMaxPingsOutstanding = 10
|
||||
}
|
||||
if c.Bus.NATSMaxReconnects == 0 {
|
||||
c.Bus.NATSMaxReconnects = -1
|
||||
}
|
||||
if c.Bus.NATSReconnectWaitSeconds == 0 {
|
||||
c.Bus.NATSReconnectWaitSeconds = 1
|
||||
}
|
||||
if c.Bus.NATSWireEncoding == "" {
|
||||
c.Bus.NATSWireEncoding = "binary_v1"
|
||||
}
|
||||
if c.EventLog.StoreKind == "" {
|
||||
c.EventLog.StoreKind = "memory"
|
||||
}
|
||||
if c.EventLog.MySQLTable == "" {
|
||||
c.EventLog.MySQLTable = defaultEventLogTable
|
||||
}
|
||||
if c.EventLog.MySQLMaxOpenConns == 0 {
|
||||
c.EventLog.MySQLMaxOpenConns = 32
|
||||
}
|
||||
if c.EventLog.MySQLMaxIdleConns == 0 {
|
||||
c.EventLog.MySQLMaxIdleConns = 8
|
||||
}
|
||||
if c.EventLog.MySQLConnMaxLifetime == 0 {
|
||||
c.EventLog.MySQLConnMaxLifetime = 300
|
||||
}
|
||||
if c.EventLog.AdminPrefix == "" {
|
||||
c.EventLog.AdminPrefix = "/eventlog"
|
||||
}
|
||||
if c.EventLog.PublishIntervalSeconds == 0 {
|
||||
c.EventLog.PublishIntervalSeconds = 1
|
||||
}
|
||||
if c.EventLog.PublishBatchSize == 0 {
|
||||
c.EventLog.PublishBatchSize = 100
|
||||
}
|
||||
if c.EventLog.PublishRetrySeconds == 0 {
|
||||
c.EventLog.PublishRetrySeconds = 1
|
||||
}
|
||||
if c.EventLog.PublishMaxAttempts == 0 {
|
||||
c.EventLog.PublishMaxAttempts = 3
|
||||
}
|
||||
if c.EventLog.PublishKind == "" {
|
||||
c.EventLog.PublishKind = "bus"
|
||||
}
|
||||
if c.EventLog.PublishHTTPMethod == "" {
|
||||
c.EventLog.PublishHTTPMethod = "POST"
|
||||
}
|
||||
if c.EventLog.PublishHTTPTimeout == 0 {
|
||||
c.EventLog.PublishHTTPTimeout = 10
|
||||
}
|
||||
if c.EventLog.PublishHTTPPayload == "" {
|
||||
c.EventLog.PublishHTTPPayload = "event"
|
||||
}
|
||||
if c.AccountCenter.StoreKind == "" {
|
||||
c.AccountCenter.StoreKind = "memory"
|
||||
}
|
||||
if c.AccountCenter.MySQLTable == "" {
|
||||
c.AccountCenter.MySQLTable = defaultAccountTable
|
||||
}
|
||||
if c.AccountCenter.AdminPrefix == "" {
|
||||
c.AccountCenter.AdminPrefix = "/accountcenter"
|
||||
}
|
||||
if c.StorageObject.StoreKind == "" {
|
||||
c.StorageObject.StoreKind = "memory"
|
||||
}
|
||||
if c.StorageObject.MySQLTable == "" {
|
||||
c.StorageObject.MySQLTable = defaultObjectTable
|
||||
}
|
||||
if c.StorageObject.AdminPrefix == "" {
|
||||
c.StorageObject.AdminPrefix = "/storage"
|
||||
}
|
||||
if c.BattleAgent.SettlementOutboxKind == "" {
|
||||
c.BattleAgent.SettlementOutboxKind = "memory"
|
||||
}
|
||||
if c.BattleAgent.SettlementOutboxStream == "" {
|
||||
c.BattleAgent.SettlementOutboxStream = defaultOutboxStream
|
||||
}
|
||||
if c.BattleAgent.SettlementOutboxType == "" {
|
||||
c.BattleAgent.SettlementOutboxType = defaultOutboxType
|
||||
}
|
||||
if c.Reload.PollIntervalSeconds == 0 {
|
||||
c.Reload.PollIntervalSeconds = 5
|
||||
}
|
||||
if c.Reload.ApplyTimeoutSeconds == 0 {
|
||||
c.Reload.ApplyTimeoutSeconds = 5
|
||||
}
|
||||
if c.HotUpdate.Target == "" {
|
||||
c.HotUpdate.Target = c.Service.Name
|
||||
}
|
||||
if c.HotUpdate.ControlKind == "" {
|
||||
c.HotUpdate.ControlKind = "memory"
|
||||
}
|
||||
if c.HotUpdate.SourceKind == "" {
|
||||
c.HotUpdate.SourceKind = "local"
|
||||
}
|
||||
if c.HotUpdate.ApplierKind == "" {
|
||||
c.HotUpdate.ApplierKind = "datatable"
|
||||
}
|
||||
if c.HotUpdate.Workspace == "" {
|
||||
c.HotUpdate.Workspace = "data/hotupdate"
|
||||
}
|
||||
if c.HotUpdate.ApplyTimeoutSeconds == 0 {
|
||||
c.HotUpdate.ApplyTimeoutSeconds = 30
|
||||
}
|
||||
if c.HotUpdate.ApplierKind == "native_patch" && c.HotUpdate.NativePatchMaxLiveSeconds == 0 {
|
||||
c.HotUpdate.NativePatchMaxLiveSeconds = 300
|
||||
}
|
||||
if c.StateSync.Kind == "" {
|
||||
c.StateSync.Kind = "memory"
|
||||
}
|
||||
if c.StateSync.Namespace == "" {
|
||||
c.StateSync.Namespace = c.Cluster.ID
|
||||
}
|
||||
if c.StateSync.TTLSeconds == 0 {
|
||||
c.StateSync.TTLSeconds = 60
|
||||
}
|
||||
if c.StateSync.CleanupIntervalSeconds == 0 {
|
||||
c.StateSync.CleanupIntervalSeconds = 30
|
||||
}
|
||||
if c.StateSync.MaxPayloadBytes == 0 {
|
||||
c.StateSync.MaxPayloadBytes = 64 * 1024
|
||||
}
|
||||
if c.ServerGroup.StoreKind == "" {
|
||||
c.ServerGroup.StoreKind = "file"
|
||||
}
|
||||
if c.ServerGroup.PlanFile == "" {
|
||||
c.ServerGroup.PlanFile = "configs/servergroup/plan.json"
|
||||
}
|
||||
if c.ServerGroup.MergeArchiveDir == "" {
|
||||
c.ServerGroup.MergeArchiveDir = "data/servergroup/merge_archives"
|
||||
}
|
||||
if c.ServerGroup.AdminPrefix == "" {
|
||||
c.ServerGroup.AdminPrefix = "/debug/servergroup"
|
||||
}
|
||||
if c.ServerGroup.InZoneDelaySeconds == 0 {
|
||||
c.ServerGroup.InZoneDelaySeconds = 24 * 60 * 60
|
||||
}
|
||||
if c.ServerGroup.NoticeLeadSeconds == 0 {
|
||||
c.ServerGroup.NoticeLeadSeconds = 24 * 60 * 60
|
||||
}
|
||||
if len(c.Topology.Services) == 0 && c.Service.Name != "" {
|
||||
c.Topology.Services = []string{c.Service.Name}
|
||||
}
|
||||
if c.Topology.GatewayService == "" {
|
||||
c.Topology.GatewayService = "gateway"
|
||||
}
|
||||
if c.Topology.GatewayBackendService == "" {
|
||||
if c.Gateway.BackendService != "" {
|
||||
c.Topology.GatewayBackendService = c.Gateway.BackendService
|
||||
} else {
|
||||
c.Topology.GatewayBackendService = "logic"
|
||||
}
|
||||
}
|
||||
if c.Topology.RefreshIntervalSeconds == 0 {
|
||||
c.Topology.RefreshIntervalSeconds = 5
|
||||
}
|
||||
if c.Discovery.CacheTTLSeconds == 0 {
|
||||
c.Discovery.CacheTTLSeconds = 1
|
||||
}
|
||||
if c.Discovery.FailureTTLSeconds == 0 {
|
||||
c.Discovery.FailureTTLSeconds = 5
|
||||
}
|
||||
if c.Discovery.FailureThreshold == 0 {
|
||||
c.Discovery.FailureThreshold = 1
|
||||
}
|
||||
if c.Discovery.Strategy == "" {
|
||||
c.Discovery.Strategy = "hash"
|
||||
}
|
||||
if c.RPC.CallTimeoutSeconds == 0 {
|
||||
c.RPC.CallTimeoutSeconds = 3
|
||||
}
|
||||
if c.RPC.MaxPending == 0 {
|
||||
c.RPC.MaxPending = 1024
|
||||
}
|
||||
if c.RPC.MaxPayloadBytes == 0 {
|
||||
c.RPC.MaxPayloadBytes = defaultRPCPayloadMax
|
||||
}
|
||||
if c.Idempotency.Kind == "" {
|
||||
c.Idempotency.Kind = "memory"
|
||||
}
|
||||
if c.Idempotency.TTLSeconds == 0 {
|
||||
c.Idempotency.TTLSeconds = 600
|
||||
}
|
||||
if c.Idempotency.KeyPrefix == "" {
|
||||
c.Idempotency.KeyPrefix = "idempotency"
|
||||
}
|
||||
if c.Idempotency.RedisAddress == "" {
|
||||
c.Idempotency.RedisAddress = "127.0.0.1:6379"
|
||||
}
|
||||
if c.Idempotency.RedisPoolSize == 0 {
|
||||
c.Idempotency.RedisPoolSize = 8
|
||||
}
|
||||
if c.Idempotency.RedisTimeoutSeconds == 0 {
|
||||
c.Idempotency.RedisTimeoutSeconds = 2
|
||||
}
|
||||
if c.Idempotency.RedisConnectTimeoutSecs == 0 {
|
||||
c.Idempotency.RedisConnectTimeoutSecs = c.Idempotency.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Idempotency.RedisReadTimeoutSecs == 0 {
|
||||
c.Idempotency.RedisReadTimeoutSecs = c.Idempotency.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Idempotency.RedisWriteTimeoutSecs == 0 {
|
||||
c.Idempotency.RedisWriteTimeoutSecs = c.Idempotency.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Idempotency.MySQLDSN == "" {
|
||||
c.Idempotency.MySQLDSN = "longheng:longheng@tcp(127.0.0.1:3306)/longheng?parseTime=true&charset=utf8mb4,utf8"
|
||||
}
|
||||
if c.Idempotency.MySQLMaxOpenConns == 0 {
|
||||
c.Idempotency.MySQLMaxOpenConns = 32
|
||||
}
|
||||
if c.Idempotency.MySQLMaxIdleConns == 0 {
|
||||
c.Idempotency.MySQLMaxIdleConns = 8
|
||||
}
|
||||
if c.Idempotency.MySQLConnMaxLifetimeSec == 0 {
|
||||
c.Idempotency.MySQLConnMaxLifetimeSec = 300
|
||||
}
|
||||
if c.Registry.Kind == "" {
|
||||
c.Registry.Kind = "memory"
|
||||
}
|
||||
if c.Registry.Namespace == "" {
|
||||
c.Registry.Namespace = "longheng"
|
||||
}
|
||||
if c.Registry.LeaseTTL == 0 {
|
||||
c.Registry.LeaseTTL = 15
|
||||
}
|
||||
if c.Bus.Kind == "" {
|
||||
c.Bus.Kind = "memory"
|
||||
}
|
||||
if c.Bus.Namespace == "" {
|
||||
c.Bus.Namespace = "longheng"
|
||||
}
|
||||
if c.Cache.Kind == "" {
|
||||
c.Cache.Kind = "memory"
|
||||
}
|
||||
if c.Cache.KeyPrefix == "" {
|
||||
c.Cache.KeyPrefix = "cache"
|
||||
}
|
||||
if c.Cache.DefaultTTLSeconds == 0 {
|
||||
c.Cache.DefaultTTLSeconds = 60
|
||||
}
|
||||
if c.Cache.MaxEntries == 0 {
|
||||
c.Cache.MaxEntries = 10000
|
||||
}
|
||||
if c.Cache.RedisAddress == "" {
|
||||
c.Cache.RedisAddress = "127.0.0.1:6379"
|
||||
}
|
||||
if c.Cache.RedisPoolSize == 0 {
|
||||
c.Cache.RedisPoolSize = 8
|
||||
}
|
||||
if c.Cache.RedisTimeoutSeconds == 0 {
|
||||
c.Cache.RedisTimeoutSeconds = 2
|
||||
}
|
||||
if c.Cache.RedisConnectTimeout == 0 {
|
||||
c.Cache.RedisConnectTimeout = c.Cache.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Cache.RedisReadTimeout == 0 {
|
||||
c.Cache.RedisReadTimeout = c.Cache.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Cache.RedisWriteTimeout == 0 {
|
||||
c.Cache.RedisWriteTimeout = c.Cache.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Lock.Kind == "" {
|
||||
c.Lock.Kind = "memory"
|
||||
}
|
||||
if c.Lock.KeyPrefix == "" {
|
||||
c.Lock.KeyPrefix = "lock"
|
||||
}
|
||||
if c.Lock.TTLSeconds == 0 {
|
||||
c.Lock.TTLSeconds = 10
|
||||
}
|
||||
if c.Lock.RedisAddress == "" {
|
||||
c.Lock.RedisAddress = "127.0.0.1:6379"
|
||||
}
|
||||
if c.Lock.RedisPoolSize == 0 {
|
||||
c.Lock.RedisPoolSize = 8
|
||||
}
|
||||
if c.Lock.RedisTimeoutSeconds == 0 {
|
||||
c.Lock.RedisTimeoutSeconds = 2
|
||||
}
|
||||
if c.Lock.RedisConnectTimeout == 0 {
|
||||
c.Lock.RedisConnectTimeout = c.Lock.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Lock.RedisReadTimeout == 0 {
|
||||
c.Lock.RedisReadTimeout = c.Lock.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Lock.RedisWriteTimeout == 0 {
|
||||
c.Lock.RedisWriteTimeout = c.Lock.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Presence.Kind == "" {
|
||||
c.Presence.Kind = "memory"
|
||||
}
|
||||
if c.Presence.KeyPrefix == "" {
|
||||
c.Presence.KeyPrefix = "presence"
|
||||
}
|
||||
if c.Presence.TTLSeconds == 0 {
|
||||
c.Presence.TTLSeconds = c.Gateway.SessionIdleTimeoutSeconds
|
||||
if c.Presence.TTLSeconds == 0 {
|
||||
c.Presence.TTLSeconds = 300
|
||||
}
|
||||
}
|
||||
if c.Presence.RedisAddress == "" {
|
||||
c.Presence.RedisAddress = "127.0.0.1:6379"
|
||||
}
|
||||
if c.Presence.RedisPoolSize == 0 {
|
||||
c.Presence.RedisPoolSize = 8
|
||||
}
|
||||
if c.Presence.RedisTimeoutSeconds == 0 {
|
||||
c.Presence.RedisTimeoutSeconds = 2
|
||||
}
|
||||
if c.Presence.RedisConnectTimeout == 0 {
|
||||
c.Presence.RedisConnectTimeout = c.Presence.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Presence.RedisReadTimeout == 0 {
|
||||
c.Presence.RedisReadTimeout = c.Presence.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Presence.RedisWriteTimeout == 0 {
|
||||
c.Presence.RedisWriteTimeout = c.Presence.RedisTimeoutSeconds
|
||||
}
|
||||
if c.Worker.Size == 0 {
|
||||
c.Worker.Size = 4
|
||||
}
|
||||
if c.Worker.Queue == 0 {
|
||||
c.Worker.Queue = 128
|
||||
}
|
||||
if c.ProfileStore.StoreKind == "" {
|
||||
c.ProfileStore.StoreKind = "memory"
|
||||
}
|
||||
if c.ProfileStore.StoreDirectory == "" {
|
||||
c.ProfileStore.StoreDirectory = "data/profiles"
|
||||
}
|
||||
if c.ProfileStore.StoreAddress == "" {
|
||||
c.ProfileStore.StoreAddress = "127.0.0.1:6379"
|
||||
}
|
||||
if c.ProfileStore.StoreKeyPrefix == "" {
|
||||
c.ProfileStore.StoreKeyPrefix = "profile"
|
||||
}
|
||||
if c.ProfileStore.StorePoolSize == 0 {
|
||||
c.ProfileStore.StorePoolSize = 8
|
||||
}
|
||||
if c.ProfileStore.StoreTimeoutSeconds == 0 {
|
||||
c.ProfileStore.StoreTimeoutSeconds = 2
|
||||
}
|
||||
if c.ProfileStore.StoreConnectTimeout == 0 {
|
||||
c.ProfileStore.StoreConnectTimeout = c.ProfileStore.StoreTimeoutSeconds
|
||||
}
|
||||
if c.ProfileStore.StoreReadTimeout == 0 {
|
||||
c.ProfileStore.StoreReadTimeout = c.ProfileStore.StoreTimeoutSeconds
|
||||
}
|
||||
if c.ProfileStore.StoreWriteTimeout == 0 {
|
||||
c.ProfileStore.StoreWriteTimeout = c.ProfileStore.StoreTimeoutSeconds
|
||||
}
|
||||
if c.ProfileStore.MySQLDSN == "" {
|
||||
c.ProfileStore.MySQLDSN = "longheng:longheng@tcp(127.0.0.1:3306)/longheng?parseTime=true&charset=utf8mb4,utf8"
|
||||
}
|
||||
if c.ProfileStore.MySQLMaxOpenConns == 0 {
|
||||
c.ProfileStore.MySQLMaxOpenConns = 32
|
||||
}
|
||||
if c.ProfileStore.MySQLMaxIdleConns == 0 {
|
||||
c.ProfileStore.MySQLMaxIdleConns = 8
|
||||
}
|
||||
if c.ProfileStore.MySQLConnMaxLifetime == 0 {
|
||||
c.ProfileStore.MySQLConnMaxLifetime = 300
|
||||
}
|
||||
if c.ProfileStore.SummaryStoreKind == "" {
|
||||
if c.ProfileStore.StoreKind == "mysql_redis" {
|
||||
c.ProfileStore.SummaryStoreKind = "redis"
|
||||
} else {
|
||||
c.ProfileStore.SummaryStoreKind = "memory"
|
||||
}
|
||||
}
|
||||
if c.ProfileStore.SummaryStoreAddress == "" {
|
||||
c.ProfileStore.SummaryStoreAddress = c.ProfileStore.StoreAddress
|
||||
}
|
||||
if c.ProfileStore.SummaryStoreKeyPrefix == "" {
|
||||
c.ProfileStore.SummaryStoreKeyPrefix = "profile_summary"
|
||||
}
|
||||
if c.ProfileStore.SummaryStorePoolSize == 0 {
|
||||
c.ProfileStore.SummaryStorePoolSize = c.ProfileStore.StorePoolSize
|
||||
}
|
||||
if c.ProfileStore.SummaryStoreTimeout == 0 {
|
||||
c.ProfileStore.SummaryStoreTimeout = c.ProfileStore.StoreTimeoutSeconds
|
||||
}
|
||||
if c.ProfileStore.SummaryStoreConnectTimeout == 0 {
|
||||
c.ProfileStore.SummaryStoreConnectTimeout = c.ProfileStore.SummaryStoreTimeout
|
||||
}
|
||||
if c.ProfileStore.SummaryStoreReadTimeout == 0 {
|
||||
c.ProfileStore.SummaryStoreReadTimeout = c.ProfileStore.SummaryStoreTimeout
|
||||
}
|
||||
if c.ProfileStore.SummaryStoreWriteTimeout == 0 {
|
||||
c.ProfileStore.SummaryStoreWriteTimeout = c.ProfileStore.SummaryStoreTimeout
|
||||
}
|
||||
if c.ProfileStore.SaveMode == "" {
|
||||
c.ProfileStore.SaveMode = "sync"
|
||||
}
|
||||
if c.ProfileStore.AsyncFlushIntervalSeconds == 0 {
|
||||
c.ProfileStore.AsyncFlushIntervalSeconds = 5
|
||||
}
|
||||
if c.ProfileStore.AsyncMaxPending == 0 {
|
||||
c.ProfileStore.AsyncMaxPending = 128
|
||||
}
|
||||
if c.ProfileStore.AsyncRetryBackoffSeconds == 0 {
|
||||
c.ProfileStore.AsyncRetryBackoffSeconds = 1
|
||||
}
|
||||
if c.ProfileStore.AsyncDeadLetterLimit == 0 {
|
||||
c.ProfileStore.AsyncDeadLetterLimit = 128
|
||||
}
|
||||
if c.ProfileStore.AsyncDeadLetterDirectory == "" {
|
||||
c.ProfileStore.AsyncDeadLetterDirectory = "data/profile_dead_letters"
|
||||
}
|
||||
if c.Gateway.AuthListen == "" {
|
||||
c.Gateway.AuthListen = ":8081"
|
||||
}
|
||||
if c.Gateway.AuthMode == "" {
|
||||
c.Gateway.AuthMode = "mock"
|
||||
}
|
||||
if c.Gateway.GateRouteFeature == "" {
|
||||
c.Gateway.GateRouteFeature = "gateway"
|
||||
}
|
||||
if c.Gateway.GateService == "" {
|
||||
if c.Topology.GatewayService != "" {
|
||||
c.Gateway.GateService = c.Topology.GatewayService
|
||||
} else {
|
||||
c.Gateway.GateService = c.Service.Name
|
||||
}
|
||||
}
|
||||
if c.Gateway.TCPListen == "" {
|
||||
c.Gateway.TCPListen = ":3341"
|
||||
}
|
||||
if c.Gateway.TCPProxyProtocolEnabled && c.Gateway.TCPProxyHeaderSec == 0 {
|
||||
c.Gateway.TCPProxyHeaderSec = 3
|
||||
}
|
||||
if c.Gateway.KCPEnabled {
|
||||
if c.Gateway.KCPListen == "" {
|
||||
c.Gateway.KCPListen = ":3342"
|
||||
}
|
||||
if c.Gateway.KCPNoDelay == 0 {
|
||||
c.Gateway.KCPNoDelay = 1
|
||||
}
|
||||
if c.Gateway.KCPInterval == 0 {
|
||||
c.Gateway.KCPInterval = 10
|
||||
}
|
||||
if c.Gateway.KCPResend == 0 {
|
||||
c.Gateway.KCPResend = 2
|
||||
}
|
||||
if c.Gateway.KCPNoCongestion == 0 {
|
||||
c.Gateway.KCPNoCongestion = 1
|
||||
}
|
||||
if c.Gateway.KCPWindowSize == 0 {
|
||||
c.Gateway.KCPWindowSize = defaultKCPWindowSize
|
||||
}
|
||||
}
|
||||
if c.Gateway.QUICEnabled {
|
||||
if c.Gateway.QUICListen == "" {
|
||||
c.Gateway.QUICListen = ":3343"
|
||||
}
|
||||
if c.Gateway.QUICHandshakeSec == 0 {
|
||||
c.Gateway.QUICHandshakeSec = 5
|
||||
}
|
||||
if c.Gateway.QUICMaxIdleTimeoutSeconds == 0 {
|
||||
c.Gateway.QUICMaxIdleTimeoutSeconds = c.Gateway.SessionIdleTimeoutSeconds
|
||||
if c.Gateway.QUICMaxIdleTimeoutSeconds == 0 {
|
||||
c.Gateway.QUICMaxIdleTimeoutSeconds = 300
|
||||
}
|
||||
}
|
||||
if c.Gateway.QUICKeepAliveSeconds == 0 {
|
||||
c.Gateway.QUICKeepAliveSeconds = 10
|
||||
}
|
||||
if c.Gateway.QUICMaxIncomingStreams == 0 {
|
||||
c.Gateway.QUICMaxIncomingStreams = defaultQUICStreams
|
||||
}
|
||||
}
|
||||
if c.Gateway.WebSocketPath == "" {
|
||||
c.Gateway.WebSocketPath = "/ws"
|
||||
}
|
||||
if c.Gateway.WebSocketEnabled && c.Gateway.WebSocketListen == "" {
|
||||
c.Gateway.WebSocketListen = ":8082"
|
||||
}
|
||||
if c.Gateway.WebSocketMaxPayloadBytes == 0 {
|
||||
c.Gateway.WebSocketMaxPayloadBytes = defaultWSPayloadMax
|
||||
}
|
||||
if c.Gateway.WebSocketHeartbeatSeconds == 0 {
|
||||
c.Gateway.WebSocketHeartbeatSeconds = defaultWSHBSeconds
|
||||
}
|
||||
if c.Gateway.ChatListen == "" {
|
||||
c.Gateway.ChatListen = "127.0.0.1:10001"
|
||||
}
|
||||
if c.Gateway.BackendService == "" {
|
||||
c.Gateway.BackendService = "logic"
|
||||
}
|
||||
if c.Gateway.BackendPollIntervalSeconds == 0 {
|
||||
c.Gateway.BackendPollIntervalSeconds = 5
|
||||
}
|
||||
if c.Gateway.MaxConnections == 0 {
|
||||
c.Gateway.MaxConnections = 1024
|
||||
}
|
||||
if c.Gateway.ConnRateWindowSec == 0 {
|
||||
c.Gateway.ConnRateWindowSec = defRateWindow
|
||||
}
|
||||
if c.Gateway.ConnectionRateLimitMax == 0 {
|
||||
c.Gateway.ConnectionRateLimitMax = defaultConnRateMax
|
||||
}
|
||||
if c.Gateway.ConnRateMaxEntries == 0 {
|
||||
c.Gateway.ConnRateMaxEntries = defConnMaxEntries
|
||||
}
|
||||
if c.Gateway.ConnRateIPv4Prefix == 0 {
|
||||
c.Gateway.ConnRateIPv4Prefix = defConnIPv4Prefix
|
||||
}
|
||||
if c.Gateway.ConnRateIPv6Prefix == 0 {
|
||||
c.Gateway.ConnRateIPv6Prefix = defConnIPv6Prefix
|
||||
}
|
||||
if c.Gateway.SessionIdleTimeoutSeconds == 0 {
|
||||
c.Gateway.SessionIdleTimeoutSeconds = 300
|
||||
}
|
||||
if c.Gateway.SessionSweepSec == 0 {
|
||||
c.Gateway.SessionSweepSec = 30
|
||||
}
|
||||
if c.Gateway.SessionHookTimeoutSeconds == 0 {
|
||||
c.Gateway.SessionHookTimeoutSeconds = 3
|
||||
}
|
||||
if c.Gateway.AccountID == "" {
|
||||
c.Gateway.AccountID = "local_account_10001"
|
||||
}
|
||||
if c.Gateway.AccountNumID == 0 {
|
||||
c.Gateway.AccountNumID = 10001
|
||||
}
|
||||
if c.Gateway.AuthMode == "mock" && c.Gateway.AuthToken == "" {
|
||||
c.Gateway.AuthToken = "local-auth-token"
|
||||
}
|
||||
if c.Gateway.AuthMode == "mock" && c.Gateway.LoginToken == "" {
|
||||
c.Gateway.LoginToken = "local-login-token"
|
||||
}
|
||||
if c.Gateway.ServerVersion == "" {
|
||||
c.Gateway.ServerVersion = "local-gateway 1.0"
|
||||
}
|
||||
if c.Gateway.ShardID == 0 {
|
||||
c.Gateway.ShardID = 9999
|
||||
}
|
||||
if c.Gateway.ShardName == "" {
|
||||
c.Gateway.ShardName = "9999"
|
||||
}
|
||||
if c.Gateway.ShardDisplay == "" {
|
||||
c.Gateway.ShardDisplay = "9999.Local"
|
||||
}
|
||||
if c.Gateway.ShardState == 0 {
|
||||
c.Gateway.ShardState = 1
|
||||
}
|
||||
if c.Gateway.Recommend == 0 {
|
||||
c.Gateway.Recommend = 1
|
||||
}
|
||||
if c.Gateway.RecommendLang == "" {
|
||||
c.Gateway.RecommendLang = "ZH"
|
||||
}
|
||||
if c.Gateway.VirtualGID == 0 {
|
||||
c.Gateway.VirtualGID = 2
|
||||
}
|
||||
if len(c.Gateway.ProductIDs) == 0 {
|
||||
c.Gateway.ProductIDs = []string{"1", "2", "3", "4", "25", "1001"}
|
||||
}
|
||||
if !c.Gateway.HasRole {
|
||||
c.Gateway.SendCreatePush = true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) trim() {
|
||||
c.Service.Name = strings.TrimSpace(c.Service.Name)
|
||||
c.Service.NodeID = strings.TrimSpace(c.Service.NodeID)
|
||||
c.Service.Environment = strings.TrimSpace(c.Service.Environment)
|
||||
c.Service.PublicAddr = strings.TrimSpace(c.Service.PublicAddr)
|
||||
c.Service.PrivateAddr = strings.TrimSpace(c.Service.PrivateAddr)
|
||||
c.Cluster.ID = strings.TrimSpace(c.Cluster.ID)
|
||||
c.Cluster.Zone = strings.TrimSpace(c.Cluster.Zone)
|
||||
c.Cluster.Version = strings.TrimSpace(c.Cluster.Version)
|
||||
c.Cluster.DataVersion = strings.TrimSpace(c.Cluster.DataVersion)
|
||||
c.Admin.Listen = strings.TrimSpace(c.Admin.Listen)
|
||||
c.Admin.Token = strings.TrimSpace(c.Admin.Token)
|
||||
for idx := range c.Admin.Tokens {
|
||||
c.Admin.Tokens[idx].Name = strings.TrimSpace(c.Admin.Tokens[idx].Name)
|
||||
c.Admin.Tokens[idx].Token = strings.TrimSpace(c.Admin.Tokens[idx].Token)
|
||||
c.Admin.Tokens[idx].Role = strings.ToLower(strings.TrimSpace(c.Admin.Tokens[idx].Role))
|
||||
c.Admin.Tokens[idx].Scopes = trimLowerStringSlice(c.Admin.Tokens[idx].Scopes)
|
||||
}
|
||||
c.OTel.Endpoint = strings.TrimSpace(c.OTel.Endpoint)
|
||||
c.OTel.ServiceName = strings.TrimSpace(c.OTel.ServiceName)
|
||||
c.DataTables.Directory = strings.TrimSpace(c.DataTables.Directory)
|
||||
c.DataTables.Version = strings.TrimSpace(c.DataTables.Version)
|
||||
c.I18N.Directory = strings.TrimSpace(c.I18N.Directory)
|
||||
c.I18N.DefaultLanguage = strings.TrimSpace(c.I18N.DefaultLanguage)
|
||||
c.I18N.Version = strings.TrimSpace(c.I18N.Version)
|
||||
c.trimPVF()
|
||||
c.Audit.Kind = strings.ToLower(strings.TrimSpace(c.Audit.Kind))
|
||||
c.Audit.FilePath = strings.TrimSpace(c.Audit.FilePath)
|
||||
c.LogicLog.Kind = strings.ToLower(strings.TrimSpace(c.LogicLog.Kind))
|
||||
c.LogicLog.FilePath = strings.TrimSpace(c.LogicLog.FilePath)
|
||||
c.LogicLog.ReasonsPath = strings.TrimSpace(c.LogicLog.ReasonsPath)
|
||||
c.BILog.Kind = strings.ToLower(strings.TrimSpace(c.BILog.Kind))
|
||||
c.BILog.FilePath = strings.TrimSpace(c.BILog.FilePath)
|
||||
c.BILog.SchemaPath = strings.TrimSpace(c.BILog.SchemaPath)
|
||||
c.EventLog.StoreKind = strings.ToLower(strings.TrimSpace(c.EventLog.StoreKind))
|
||||
c.EventLog.MySQLDSN = strings.TrimSpace(c.EventLog.MySQLDSN)
|
||||
c.EventLog.MySQLTable = strings.TrimSpace(c.EventLog.MySQLTable)
|
||||
c.EventLog.AdminPrefix = strings.TrimSpace(c.EventLog.AdminPrefix)
|
||||
c.EventLog.PublishKind = strings.ToLower(strings.TrimSpace(c.EventLog.PublishKind))
|
||||
c.EventLog.PublishHTTPURL = strings.TrimSpace(c.EventLog.PublishHTTPURL)
|
||||
c.EventLog.PublishHTTPMethod = strings.ToUpper(strings.TrimSpace(c.EventLog.PublishHTTPMethod))
|
||||
c.EventLog.PublishHTTPPayload = strings.ToLower(strings.TrimSpace(c.EventLog.PublishHTTPPayload))
|
||||
c.EventLog.PublishHTTPHeaders = trimStringMap(c.EventLog.PublishHTTPHeaders)
|
||||
c.AccountCenter.StoreKind = strings.ToLower(strings.TrimSpace(c.AccountCenter.StoreKind))
|
||||
c.AccountCenter.MySQLDSN = strings.TrimSpace(c.AccountCenter.MySQLDSN)
|
||||
c.AccountCenter.MySQLTable = strings.TrimSpace(c.AccountCenter.MySQLTable)
|
||||
c.AccountCenter.AdminPrefix = strings.TrimSpace(c.AccountCenter.AdminPrefix)
|
||||
c.StorageObject.StoreKind = strings.ToLower(strings.TrimSpace(c.StorageObject.StoreKind))
|
||||
c.StorageObject.MySQLDSN = strings.TrimSpace(c.StorageObject.MySQLDSN)
|
||||
c.StorageObject.MySQLTable = strings.TrimSpace(c.StorageObject.MySQLTable)
|
||||
c.StorageObject.AdminPrefix = strings.TrimSpace(c.StorageObject.AdminPrefix)
|
||||
c.BattleAgent.SettlementOutboxKind = strings.ToLower(strings.TrimSpace(c.BattleAgent.SettlementOutboxKind))
|
||||
c.BattleAgent.SettlementOutboxStream = strings.TrimSpace(c.BattleAgent.SettlementOutboxStream)
|
||||
c.BattleAgent.SettlementOutboxType = strings.TrimSpace(c.BattleAgent.SettlementOutboxType)
|
||||
c.HotUpdate.Target = strings.TrimSpace(c.HotUpdate.Target)
|
||||
c.HotUpdate.ControlKind = strings.ToLower(strings.TrimSpace(c.HotUpdate.ControlKind))
|
||||
c.HotUpdate.SourceKind = strings.ToLower(strings.TrimSpace(c.HotUpdate.SourceKind))
|
||||
c.HotUpdate.ApplierKind = strings.ToLower(strings.TrimSpace(c.HotUpdate.ApplierKind))
|
||||
c.HotUpdate.Workspace = strings.TrimSpace(c.HotUpdate.Workspace)
|
||||
c.HotUpdate.SigningKeyEnv = strings.TrimSpace(c.HotUpdate.SigningKeyEnv)
|
||||
c.HotUpdate.NativePatchAllowedTargets = trimStringSlice(c.HotUpdate.NativePatchAllowedTargets)
|
||||
c.HotUpdate.PatchOldSymbols = trimStringSlice(c.HotUpdate.PatchOldSymbols)
|
||||
c.StateSync.Kind = strings.ToLower(strings.TrimSpace(c.StateSync.Kind))
|
||||
c.StateSync.Namespace = strings.TrimSpace(c.StateSync.Namespace)
|
||||
c.ServerGroup.StoreKind = strings.ToLower(strings.TrimSpace(c.ServerGroup.StoreKind))
|
||||
c.ServerGroup.PlanFile = strings.TrimSpace(c.ServerGroup.PlanFile)
|
||||
c.ServerGroup.MergeArchiveDir = strings.TrimSpace(c.ServerGroup.MergeArchiveDir)
|
||||
c.ServerGroup.AdminPrefix = strings.TrimSpace(c.ServerGroup.AdminPrefix)
|
||||
c.Topology.Services = trimStringSlice(c.Topology.Services)
|
||||
c.Topology.GatewayService = strings.TrimSpace(c.Topology.GatewayService)
|
||||
c.Topology.GatewayBackendService = strings.TrimSpace(c.Topology.GatewayBackendService)
|
||||
c.Discovery.Strategy = strings.ToLower(strings.TrimSpace(c.Discovery.Strategy))
|
||||
c.RateLimit.Algorithm = strings.ToLower(strings.TrimSpace(c.RateLimit.Algorithm))
|
||||
if c.RateLimit.Algorithm == "tokenbucket" || c.RateLimit.Algorithm == "token-bucket" {
|
||||
c.RateLimit.Algorithm = "token_bucket"
|
||||
}
|
||||
c.RateLimit.Rules = strings.TrimSpace(c.RateLimit.Rules)
|
||||
c.RateLimit.TrustedHeader = strings.TrimSpace(c.RateLimit.TrustedHeader)
|
||||
c.Idempotency.Kind = strings.ToLower(strings.TrimSpace(c.Idempotency.Kind))
|
||||
c.Idempotency.KeyPrefix = strings.TrimSpace(c.Idempotency.KeyPrefix)
|
||||
c.Idempotency.RedisAddress = strings.TrimSpace(c.Idempotency.RedisAddress)
|
||||
c.Idempotency.RedisPassword = strings.TrimSpace(c.Idempotency.RedisPassword)
|
||||
c.Idempotency.MySQLDSN = strings.TrimSpace(c.Idempotency.MySQLDSN)
|
||||
c.Registry.Kind = strings.ToLower(strings.TrimSpace(c.Registry.Kind))
|
||||
c.Registry.Namespace = strings.TrimSpace(c.Registry.Namespace)
|
||||
c.Registry.Endpoints = trimStringSlice(c.Registry.Endpoints)
|
||||
c.Registry.RedisPassword = strings.TrimSpace(c.Registry.RedisPassword)
|
||||
c.Bus.Kind = strings.ToLower(strings.TrimSpace(c.Bus.Kind))
|
||||
c.Bus.Namespace = strings.TrimSpace(c.Bus.Namespace)
|
||||
c.Bus.Endpoints = trimStringSlice(c.Bus.Endpoints)
|
||||
c.Bus.RedisUsername = strings.TrimSpace(c.Bus.RedisUsername)
|
||||
c.Bus.RedisPassword = strings.TrimSpace(c.Bus.RedisPassword)
|
||||
c.Bus.RedisTLSServerName = strings.TrimSpace(c.Bus.RedisTLSServerName)
|
||||
c.Bus.NATSToken = strings.TrimSpace(c.Bus.NATSToken)
|
||||
c.Bus.NATSUsername = strings.TrimSpace(c.Bus.NATSUsername)
|
||||
c.Bus.NATSPassword = strings.TrimSpace(c.Bus.NATSPassword)
|
||||
c.Bus.NATSCredentials = strings.TrimSpace(c.Bus.NATSCredentials)
|
||||
c.Bus.NATSTLSServerName = strings.TrimSpace(c.Bus.NATSTLSServerName)
|
||||
c.Bus.NATSCAFile = strings.TrimSpace(c.Bus.NATSCAFile)
|
||||
c.Bus.NATSCertFile = strings.TrimSpace(c.Bus.NATSCertFile)
|
||||
c.Bus.NATSKeyFile = strings.TrimSpace(c.Bus.NATSKeyFile)
|
||||
c.Cache.Kind = strings.ToLower(strings.TrimSpace(c.Cache.Kind))
|
||||
c.Cache.KeyPrefix = strings.TrimSpace(c.Cache.KeyPrefix)
|
||||
c.Cache.RedisAddress = strings.TrimSpace(c.Cache.RedisAddress)
|
||||
c.Cache.RedisPassword = strings.TrimSpace(c.Cache.RedisPassword)
|
||||
c.Lock.Kind = strings.ToLower(strings.TrimSpace(c.Lock.Kind))
|
||||
c.Lock.KeyPrefix = strings.TrimSpace(c.Lock.KeyPrefix)
|
||||
c.Lock.RedisAddress = strings.TrimSpace(c.Lock.RedisAddress)
|
||||
c.Lock.RedisPassword = strings.TrimSpace(c.Lock.RedisPassword)
|
||||
c.Presence.Kind = strings.ToLower(strings.TrimSpace(c.Presence.Kind))
|
||||
c.Presence.KeyPrefix = strings.TrimSpace(c.Presence.KeyPrefix)
|
||||
c.Presence.RedisAddress = strings.TrimSpace(c.Presence.RedisAddress)
|
||||
c.Presence.RedisPassword = strings.TrimSpace(c.Presence.RedisPassword)
|
||||
c.ProfileStore.StoreKind = strings.ToLower(strings.TrimSpace(c.ProfileStore.StoreKind))
|
||||
c.ProfileStore.StoreDirectory = strings.TrimSpace(c.ProfileStore.StoreDirectory)
|
||||
c.ProfileStore.StoreAddress = strings.TrimSpace(c.ProfileStore.StoreAddress)
|
||||
c.ProfileStore.StorePassword = strings.TrimSpace(c.ProfileStore.StorePassword)
|
||||
c.ProfileStore.StoreKeyPrefix = strings.TrimSpace(c.ProfileStore.StoreKeyPrefix)
|
||||
c.ProfileStore.MySQLDSN = strings.TrimSpace(c.ProfileStore.MySQLDSN)
|
||||
for idx := range c.ProfileStore.MySQLShards {
|
||||
c.ProfileStore.MySQLShards[idx].ID = strings.TrimSpace(c.ProfileStore.MySQLShards[idx].ID)
|
||||
c.ProfileStore.MySQLShards[idx].DSN = strings.TrimSpace(c.ProfileStore.MySQLShards[idx].DSN)
|
||||
c.ProfileStore.MySQLShards[idx].TableName = strings.TrimSpace(c.ProfileStore.MySQLShards[idx].TableName)
|
||||
c.ProfileStore.MySQLShards[idx].TablePrefix = strings.TrimSpace(c.ProfileStore.MySQLShards[idx].TablePrefix)
|
||||
c.ProfileStore.MySQLShards[idx].HashSlots = strings.TrimSpace(c.ProfileStore.MySQLShards[idx].HashSlots)
|
||||
}
|
||||
c.ProfileStore.SummaryStoreKind = strings.ToLower(strings.TrimSpace(c.ProfileStore.SummaryStoreKind))
|
||||
c.ProfileStore.SummaryStoreAddress = strings.TrimSpace(c.ProfileStore.SummaryStoreAddress)
|
||||
c.ProfileStore.SummaryStorePassword = strings.TrimSpace(c.ProfileStore.SummaryStorePassword)
|
||||
c.ProfileStore.SummaryStoreKeyPrefix = strings.TrimSpace(c.ProfileStore.SummaryStoreKeyPrefix)
|
||||
c.ProfileStore.SaveMode = strings.ToLower(strings.TrimSpace(c.ProfileStore.SaveMode))
|
||||
c.ProfileStore.AsyncDeadLetterDirectory = strings.TrimSpace(c.ProfileStore.AsyncDeadLetterDirectory)
|
||||
c.Gateway.AuthListen = strings.TrimSpace(c.Gateway.AuthListen)
|
||||
c.Gateway.AuthMode = strings.ToLower(strings.TrimSpace(c.Gateway.AuthMode))
|
||||
c.Gateway.GateRouteFeature = strings.TrimSpace(c.Gateway.GateRouteFeature)
|
||||
c.Gateway.GateService = strings.TrimSpace(c.Gateway.GateService)
|
||||
c.Gateway.TCPListen = strings.TrimSpace(c.Gateway.TCPListen)
|
||||
c.Gateway.TCPProxyTrustedCIDRs = trimStringSlice(c.Gateway.TCPProxyTrustedCIDRs)
|
||||
c.Gateway.KCPListen = strings.TrimSpace(c.Gateway.KCPListen)
|
||||
c.Gateway.QUICListen = strings.TrimSpace(c.Gateway.QUICListen)
|
||||
c.Gateway.QUICCertFile = strings.TrimSpace(c.Gateway.QUICCertFile)
|
||||
c.Gateway.QUICKeyFile = strings.TrimSpace(c.Gateway.QUICKeyFile)
|
||||
c.Gateway.WebSocketListen = strings.TrimSpace(c.Gateway.WebSocketListen)
|
||||
c.Gateway.WebSocketPath = strings.TrimSpace(c.Gateway.WebSocketPath)
|
||||
c.Gateway.ChatListen = strings.TrimSpace(c.Gateway.ChatListen)
|
||||
c.Gateway.BackendService = strings.TrimSpace(c.Gateway.BackendService)
|
||||
c.Gateway.AccountID = strings.TrimSpace(c.Gateway.AccountID)
|
||||
c.Gateway.SessionKeyMasterSecret = strings.TrimSpace(c.Gateway.SessionKeyMasterSecret)
|
||||
c.Gateway.AuthToken = strings.TrimSpace(c.Gateway.AuthToken)
|
||||
c.Gateway.LoginToken = strings.TrimSpace(c.Gateway.LoginToken)
|
||||
c.Gateway.ServerVersion = strings.TrimSpace(c.Gateway.ServerVersion)
|
||||
c.Gateway.ShardName = strings.TrimSpace(c.Gateway.ShardName)
|
||||
c.Gateway.ShardDisplay = strings.TrimSpace(c.Gateway.ShardDisplay)
|
||||
c.Gateway.RecommendLang = strings.TrimSpace(c.Gateway.RecommendLang)
|
||||
c.Gateway.ProductIDs = trimStringSlice(c.Gateway.ProductIDs)
|
||||
}
|
||||
|
||||
func trimStringSlice(values []string) []string {
|
||||
for idx := range values {
|
||||
values[idx] = strings.TrimSpace(values[idx])
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func trimLowerStringSlice(values []string) []string {
|
||||
for idx := range values {
|
||||
values[idx] = strings.ToLower(strings.TrimSpace(values[idx]))
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func trimStringMap(values map[string]string) map[string]string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(values))
|
||||
for key, value := range values {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
out[key] = strings.TrimSpace(value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
defaultPVFMaxBytes int64 = 1024 * 1024 * 1024
|
||||
defaultPVFChunkCacheBytes int64 = 96 * 1024 * 1024
|
||||
defaultPVFTextCacheBytes int64 = 64 * 1024 * 1024
|
||||
)
|
||||
|
||||
// PVFSection is the DNF project extension that keeps Script.pvf loading in
|
||||
// the generic platform bootstrap without coupling the framework to DNF logic.
|
||||
type PVFSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Path string `toml:"path"`
|
||||
MaxBytes int64 `toml:"max_bytes"`
|
||||
PreloadChunks bool `toml:"preload_chunks"`
|
||||
ChunkCacheBytes int64 `toml:"chunk_cache_bytes"`
|
||||
TextCacheBytes int64 `toml:"text_cache_bytes"`
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) normalizePVF() {
|
||||
if c.PVF.MaxBytes == 0 {
|
||||
c.PVF.MaxBytes = defaultPVFMaxBytes
|
||||
}
|
||||
if c.PVF.ChunkCacheBytes == 0 {
|
||||
c.PVF.ChunkCacheBytes = defaultPVFChunkCacheBytes
|
||||
}
|
||||
if c.PVF.TextCacheBytes == 0 {
|
||||
c.PVF.TextCacheBytes = defaultPVFTextCacheBytes
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) trimPVF() {
|
||||
c.PVF.Path = strings.TrimSpace(c.PVF.Path)
|
||||
}
|
||||
|
||||
func (c ServiceConfig) validatePVF(errs *[]error) {
|
||||
if c.PVF.Enabled {
|
||||
requireString(errs, "pvf.path", c.PVF.Path)
|
||||
}
|
||||
if c.PVF.MaxBytes != -1 {
|
||||
requirePositiveInt64(errs, "pvf.max_bytes", c.PVF.MaxBytes)
|
||||
}
|
||||
requirePositiveInt64(errs, "pvf.chunk_cache_bytes", c.PVF.ChunkCacheBytes)
|
||||
requirePositiveInt64(errs, "pvf.text_cache_bytes", c.PVF.TextCacheBytes)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizePVFUsesOneGiBDefaultLimit(t *testing.T) {
|
||||
var cfg ServiceConfig
|
||||
cfg.normalizePVF()
|
||||
const want int64 = 1024 * 1024 * 1024
|
||||
if cfg.PVF.MaxBytes != want {
|
||||
t.Fatalf("PVF.MaxBytes = %d, want %d", cfg.PVF.MaxBytes, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
func (s ServiceSection) StopTimeout() time.Duration {
|
||||
return time.Duration(s.StopTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (s ServiceSection) StopInitialGrace() time.Duration {
|
||||
return time.Duration(s.StopInitialGraceSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (s ServiceSection) StopProbeInterval() time.Duration {
|
||||
return time.Duration(s.StopProbeSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (s ServiceSection) StopStableWindow() time.Duration {
|
||||
return time.Duration(s.StopStableSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (a AdminSection) ReadTimeout() time.Duration {
|
||||
return time.Duration(a.ReadTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (a AdminSection) ReadHeaderTimeout() time.Duration {
|
||||
return time.Duration(a.ReadHeaderTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (a AdminSection) WriteTimeout() time.Duration {
|
||||
return time.Duration(a.WriteTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (a AdminSection) IdleTimeout() time.Duration {
|
||||
return time.Duration(a.IdleTimeoutSeconds) * time.Second
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var configEnvPattern = regexp.MustCompile(`\$\{[A-Za-z_][A-Za-z0-9_]*\}`)
|
||||
|
||||
func expandEnvString(value string) string {
|
||||
return configEnvPattern.ReplaceAllStringFunc(value, func(token string) string {
|
||||
key := token[2 : len(token)-1]
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return token
|
||||
})
|
||||
}
|
||||
|
||||
func expandEnvValues(target any) {
|
||||
expandConfigEnvValue(reflect.ValueOf(target))
|
||||
}
|
||||
|
||||
func expandConfigEnvValue(value reflect.Value) {
|
||||
if !value.IsValid() {
|
||||
return
|
||||
}
|
||||
if value.Kind() == reflect.Pointer {
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
expandConfigEnvValue(value.Elem())
|
||||
return
|
||||
}
|
||||
switch value.Kind() {
|
||||
case reflect.Struct:
|
||||
for idx := 0; idx < value.NumField(); idx++ {
|
||||
field := value.Field(idx)
|
||||
if !field.CanSet() && field.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
expandConfigEnvValue(field)
|
||||
}
|
||||
case reflect.String:
|
||||
if value.CanSet() {
|
||||
value.SetString(expandEnvString(value.String()))
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for idx := 0; idx < value.Len(); idx++ {
|
||||
expandConfigEnvValue(value.Index(idx))
|
||||
}
|
||||
case reflect.Map:
|
||||
if value.IsNil() || !value.CanSet() {
|
||||
return
|
||||
}
|
||||
replacement := reflect.MakeMapWithSize(value.Type(), value.Len())
|
||||
for _, key := range value.MapKeys() {
|
||||
outKey := key
|
||||
outValue := value.MapIndex(key)
|
||||
if key.Kind() == reflect.String {
|
||||
outKey = reflect.ValueOf(expandEnvString(key.String())).Convert(key.Type())
|
||||
}
|
||||
if outValue.Kind() == reflect.String {
|
||||
outValue = reflect.ValueOf(expandEnvString(outValue.String())).Convert(outValue.Type())
|
||||
}
|
||||
replacement.SetMapIndex(outKey, outValue)
|
||||
}
|
||||
value.Set(replacement)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validateProdSafety(errs *[]error, cfg ServiceConfig) {
|
||||
// 生产类环境不能只看 environment=prod,k8s/helm/staging 也按上线标准 fail-fast。
|
||||
validEnvPlaceholders(errs, reflect.ValueOf(cfg), "")
|
||||
rejectProdDefault(errs, "cluster.id", cfg.Cluster.ID)
|
||||
rejectProdDefault(errs, "cluster.version", cfg.Cluster.Version)
|
||||
rejectProdDefault(errs, "cluster.data_version", cfg.Cluster.DataVersion)
|
||||
rejectKnownSecret(errs, "admin.token", cfg.Admin.Token)
|
||||
if len(cfg.Admin.Tokens) == 0 {
|
||||
*errs = append(*errs, fmt.Errorf("admin.tokens requires at least one scoped token for production-like environments"))
|
||||
}
|
||||
if !cfg.Metrics.RequireAdminToken {
|
||||
*errs = append(*errs, fmt.Errorf("metrics.require_admin_token is required for production-like environments"))
|
||||
}
|
||||
if cfg.HotUpdate.Enabled && !cfg.HotUpdate.RequireSignature {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.require_signature is required for production-like environments"))
|
||||
}
|
||||
if cfg.Bus.Kind == "" || cfg.Bus.Kind == "memory" {
|
||||
*errs = append(*errs, fmt.Errorf("bus.kind memory is not allowed for production-like environments"))
|
||||
}
|
||||
if cfg.Cache.Kind == "" || cfg.Cache.Kind == "memory" {
|
||||
*errs = append(*errs, fmt.Errorf("cache.kind memory is not allowed for production-like environments"))
|
||||
}
|
||||
if cfg.Lock.Kind == "" || cfg.Lock.Kind == "memory" {
|
||||
*errs = append(*errs, fmt.Errorf("lock.kind memory is not allowed for production-like environments"))
|
||||
}
|
||||
if cfg.Registry.Kind == "" || cfg.Registry.Kind == "memory" {
|
||||
*errs = append(*errs, fmt.Errorf("registry.kind memory is not allowed for production-like environments"))
|
||||
}
|
||||
if !cfg.Tracing.Enabled && !cfg.OTel.Enabled {
|
||||
*errs = append(*errs, fmt.Errorf("tracing.enabled or otel.enabled is required for production-like environments"))
|
||||
}
|
||||
if cfg.Audit.Kind != "file" {
|
||||
*errs = append(*errs, fmt.Errorf("audit.kind file is required for production-like environments"))
|
||||
}
|
||||
if sdkInsecureEnv() {
|
||||
*errs = append(*errs, fmt.Errorf("LONGHENG_SDK_INSECURE must not be enabled for production-like environments"))
|
||||
}
|
||||
for idx, token := range cfg.Admin.Tokens {
|
||||
rejectKnownSecret(errs, fmt.Sprintf("admin.tokens[%d].token", idx), token.Token)
|
||||
}
|
||||
if isGatewayService(cfg.Service.Name) {
|
||||
// 多 gateway 必须共享在线状态,否则跨网关推送、踢人和重连恢复都会失真。
|
||||
if cfg.Presence.Kind != "redis" {
|
||||
*errs = append(*errs, fmt.Errorf("presence.kind redis is required for production gateway services"))
|
||||
}
|
||||
if !cfg.Gateway.ConnectionRateLimitEnabled {
|
||||
*errs = append(*errs, fmt.Errorf("gateway.connection_rate_limit_enabled is required for production gateway services"))
|
||||
}
|
||||
if cfg.Gateway.AuthToken != "" {
|
||||
rejectKnownSecret(errs, "gateway.auth_token", cfg.Gateway.AuthToken)
|
||||
}
|
||||
if cfg.Gateway.LoginToken != "" {
|
||||
rejectKnownSecret(errs, "gateway.login_token", cfg.Gateway.LoginToken)
|
||||
}
|
||||
requireProdSecret(errs, "gateway.session_key_master_secret", cfg.Gateway.SessionKeyMasterSecret)
|
||||
}
|
||||
if cfg.Idempotency.Kind == "redis" || cfg.Idempotency.Kind == "mysql_redis" {
|
||||
requireProdSecret(errs, "idempotency.redis_password", cfg.Idempotency.RedisPassword)
|
||||
}
|
||||
if cfg.Registry.Kind == "redis" {
|
||||
requireProdSecret(errs, "registry.redis_password", cfg.Registry.RedisPassword)
|
||||
}
|
||||
if cfg.Bus.Kind == "redis" {
|
||||
requireProdSecret(errs, "bus.redis_password", cfg.Bus.RedisPassword)
|
||||
if !cfg.Bus.RedisTLSEnabled {
|
||||
*errs = append(*errs, fmt.Errorf("bus.redis_tls_enabled is required for production-like redis bus"))
|
||||
}
|
||||
}
|
||||
if cfg.Bus.Kind == "nats" {
|
||||
if strings.TrimSpace(cfg.Bus.NATSToken) == "" && strings.TrimSpace(cfg.Bus.NATSCredentials) == "" &&
|
||||
(strings.TrimSpace(cfg.Bus.NATSUsername) == "" || strings.TrimSpace(cfg.Bus.NATSPassword) == "") {
|
||||
*errs = append(*errs, fmt.Errorf("bus nats requires token, credentials, or username/password for production-like environments"))
|
||||
}
|
||||
if strings.TrimSpace(cfg.Bus.NATSToken) != "" {
|
||||
rejectKnownSecret(errs, "bus.nats_token", cfg.Bus.NATSToken)
|
||||
}
|
||||
if strings.TrimSpace(cfg.Bus.NATSPassword) != "" {
|
||||
rejectKnownSecret(errs, "bus.nats_password", cfg.Bus.NATSPassword)
|
||||
}
|
||||
if !cfg.Bus.NATSTLSEnabled && strings.TrimSpace(cfg.Bus.NATSCAFile) == "" && strings.TrimSpace(cfg.Bus.NATSCertFile) == "" {
|
||||
*errs = append(*errs, fmt.Errorf("bus.nats_tls_enabled or nats CA/client cert is required for production-like nats bus"))
|
||||
}
|
||||
}
|
||||
if cfg.Cache.Kind == "redis" {
|
||||
requireProdSecret(errs, "cache.redis_password", cfg.Cache.RedisPassword)
|
||||
}
|
||||
if cfg.Lock.Kind == "redis" {
|
||||
requireProdSecret(errs, "lock.redis_password", cfg.Lock.RedisPassword)
|
||||
}
|
||||
if cfg.Presence.Kind == "redis" {
|
||||
requireProdSecret(errs, "presence.redis_password", cfg.Presence.RedisPassword)
|
||||
}
|
||||
if cfg.ProfileStore.StoreKind == "redis" || cfg.ProfileStore.StoreKind == "pika" || cfg.ProfileStore.StoreKind == "profiledb" || cfg.ProfileStore.StoreKind == "profile_db" || cfg.ProfileStore.StoreKind == "mysql_redis" {
|
||||
requireProdSecret(errs, "profile_store.store_password", cfg.ProfileStore.StorePassword)
|
||||
}
|
||||
if cfg.ProfileStore.SummaryStoreKind == "redis" || cfg.ProfileStore.SummaryStoreKind == "pika" {
|
||||
requireProdSecret(errs, "profile_store.summary_store_password", cfg.ProfileStore.SummaryStorePassword)
|
||||
}
|
||||
if isProfileCriticalSvc(cfg.Service.Name) {
|
||||
// logic/playerd 是 Profile 权威写路径,生产不能退回文件、内存或纯 Redis 存档。
|
||||
switch cfg.ProfileStore.StoreKind {
|
||||
case "mysql", "mysql_redis":
|
||||
default:
|
||||
*errs = append(*errs, fmt.Errorf("profile_store.store_kind mysql or mysql_redis is required for production %s services", cfg.Service.Name))
|
||||
}
|
||||
switch cfg.ProfileStore.SummaryStoreKind {
|
||||
case "redis", "pika":
|
||||
default:
|
||||
*errs = append(*errs, fmt.Errorf("profile_store.summary_store_kind redis or pika is required for production %s services", cfg.Service.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sdkInsecureEnv() bool {
|
||||
switch strings.ToLower(strings.TrimSpace(os.Getenv("LONGHENG_SDK_INSECURE"))) {
|
||||
case "1", "true", "yes", "y", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validEnvPlaceholders(errs *[]error, value reflect.Value, path string) {
|
||||
if !value.IsValid() {
|
||||
return
|
||||
}
|
||||
if value.Kind() == reflect.Pointer {
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
validEnvPlaceholders(errs, value.Elem(), path)
|
||||
return
|
||||
}
|
||||
switch value.Kind() {
|
||||
case reflect.Struct:
|
||||
valueType := value.Type()
|
||||
for idx := 0; idx < value.NumField(); idx++ {
|
||||
fieldType := valueType.Field(idx)
|
||||
if fieldType.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
fieldPath := joinConfigPath(path, tomlFieldName(fieldType))
|
||||
validEnvPlaceholders(errs, value.Field(idx), fieldPath)
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
for idx := 0; idx < value.Len(); idx++ {
|
||||
validEnvPlaceholders(errs, value.Index(idx), fmt.Sprintf("%s[%d]", path, idx))
|
||||
}
|
||||
case reflect.String:
|
||||
text := value.String()
|
||||
if configEnvPattern.MatchString(text) || isShellEnvOnly(text) {
|
||||
// 未解析占位符比空值更危险:看起来非空,实际会把字面量当密钥或地址启动。
|
||||
*errs = append(*errs, fmt.Errorf("%s contains unresolved environment variable placeholder", path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tomlFieldName(field reflect.StructField) string {
|
||||
tag := field.Tag.Get("toml")
|
||||
if tag == "" || tag == "-" {
|
||||
return strings.ToLower(field.Name)
|
||||
}
|
||||
name := strings.Split(tag, ",")[0]
|
||||
if name == "" {
|
||||
return strings.ToLower(field.Name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func joinConfigPath(prefix, name string) string {
|
||||
if prefix == "" {
|
||||
return name
|
||||
}
|
||||
return prefix + "." + name
|
||||
}
|
||||
|
||||
func isShellEnvOnly(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) < 2 || value[0] != '$' {
|
||||
return false
|
||||
}
|
||||
for idx, r := range value[1:] {
|
||||
if idx == 0 {
|
||||
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func requireProdSecret(errs *[]error, field, value string) {
|
||||
requireString(errs, field, value)
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
// 非空还不够,常见本地默认值也要直接拒绝。
|
||||
rejectKnownSecret(errs, field, value)
|
||||
}
|
||||
|
||||
func rejectKnownSecret(errs *[]error, field, value string) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "local-admin-token",
|
||||
"local-login-token",
|
||||
"local-auth-token",
|
||||
"local-session-key-master-secret",
|
||||
"local-token",
|
||||
"admin",
|
||||
"token",
|
||||
"password",
|
||||
"redis",
|
||||
"mysql",
|
||||
"changeme",
|
||||
"change-me",
|
||||
"change_me":
|
||||
*errs = append(*errs, fmt.Errorf("%s must not use a local/default secret in production-like environments", field))
|
||||
}
|
||||
}
|
||||
|
||||
func rejectProdDefault(errs *[]error, field, value string) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "local", "default", "changeme", "change-me", "change_me":
|
||||
*errs = append(*errs, fmt.Errorf("%s must not use a local/default value in production-like environments", field))
|
||||
}
|
||||
}
|
||||
|
||||
func IsProdLikeEnv(environment string) bool {
|
||||
return isProdLikeEnv(environment)
|
||||
}
|
||||
|
||||
// UsesDistributedRuntimeAuthority 判断配置是否已经启用分布式或持久化权威路径,
|
||||
// 用于承载有状态运行时决策。
|
||||
//
|
||||
// 它不会单独把整份配置判定为生产环境:本地开发也可能使用 MySQL/Redis。
|
||||
// 该信号只服务于运行时装配和预检,避免调用方只依赖 service.environment。
|
||||
func UsesDistributedRuntimeAuthority(cfg ServiceConfig) bool {
|
||||
if cfg.Topology.Enabled {
|
||||
return true
|
||||
}
|
||||
if isProfileCriticalSvc(cfg.Service.Name) && ProfileStoreUsesDurableAuthority(cfg.ProfileStore) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ProfileStoreUsesDurableAuthority 判断 profile_store.store_kind 是否由持久化 DB
|
||||
// 作为权威存储,而不是进程本地或仅文件的开发存储。
|
||||
func ProfileStoreUsesDurableAuthority(section ProfileStoreSection) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(section.StoreKind)) {
|
||||
case "mysql", "mysql_redis":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isProdLikeEnv(environment string) bool {
|
||||
environment = strings.ToLower(strings.TrimSpace(environment))
|
||||
if environment == "" {
|
||||
return false
|
||||
}
|
||||
for _, token := range []string{"prod", "production", "staging", "stage", "preprod", "pre-production", "k8s", "kubernetes", "helm", "cluster", "live"} {
|
||||
if strings.Contains(environment, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isGatewayService(serviceName string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(serviceName), "gateway")
|
||||
}
|
||||
|
||||
func isEventLogCritical(serviceName string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(serviceName)) {
|
||||
case "gateway", "logic", "playerd", "payx":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isIdemCriticalSvc(serviceName string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(serviceName)) {
|
||||
case "logic", "playerd":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isProfileCriticalSvc(serviceName string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(serviceName)) {
|
||||
case "logic", "playerd":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isBattleAgentService(serviceName string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(serviceName), "battleagent")
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validateMySQLShards(errs *[]error, section ProfileStoreSection) {
|
||||
if !section.MySQLShardingEnabled && len(section.MySQLShards) == 0 {
|
||||
return
|
||||
}
|
||||
if len(section.MySQLShards) == 0 {
|
||||
*errs = append(*errs, fmt.Errorf("profile_store.mysql_shards is required when mysql_sharding_enabled is true"))
|
||||
return
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
useHashSlots := false
|
||||
hashSlotOwners := make([]string, 1024)
|
||||
for idx, shard := range section.MySQLShards {
|
||||
field := fmt.Sprintf("profile_store.mysql_shards[%d]", idx)
|
||||
requireSafeName(errs, field+".id", shard.ID)
|
||||
if _, ok := seen[shard.ID]; ok {
|
||||
*errs = append(*errs, fmt.Errorf("%s.id %q is duplicated", field, shard.ID))
|
||||
}
|
||||
seen[shard.ID] = struct{}{}
|
||||
if strings.TrimSpace(shard.DSN) == "" && strings.TrimSpace(section.MySQLDSN) == "" {
|
||||
*errs = append(*errs, fmt.Errorf("%s.dsn is required when profile_store.mysql_dsn is empty", field))
|
||||
}
|
||||
if strings.TrimSpace(shard.TableName) != "" {
|
||||
requireSQLIdentifier(errs, field+".table_name", shard.TableName)
|
||||
}
|
||||
if strings.TrimSpace(shard.TablePrefix) != "" {
|
||||
requireSQLIdentifier(errs, field+".table_prefix", shard.TablePrefix)
|
||||
}
|
||||
if strings.TrimSpace(shard.TableName) != "" && strings.TrimSpace(shard.TablePrefix) != "" {
|
||||
*errs = append(*errs, fmt.Errorf("%s can set table_name or table_prefix, not both", field))
|
||||
}
|
||||
if strings.TrimSpace(shard.HashSlots) != "" {
|
||||
useHashSlots = true
|
||||
ranges, err := parseHashSlotRanges(shard.HashSlots)
|
||||
if err != nil {
|
||||
*errs = append(*errs, fmt.Errorf("%s.hash_slots %w", field, err))
|
||||
} else {
|
||||
for _, item := range ranges {
|
||||
for slot := item.start; slot <= item.end; slot++ {
|
||||
if hashSlotOwners[slot] != "" {
|
||||
*errs = append(*errs, fmt.Errorf("%s.hash_slots overlaps slot %d with shard %q", field, slot, hashSlotOwners[slot]))
|
||||
}
|
||||
hashSlotOwners[slot] = shard.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if shard.MaxOpenConns < 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s.max_open_conns must be non-negative", field))
|
||||
}
|
||||
if shard.MaxIdleConns < 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s.max_idle_conns must be non-negative", field))
|
||||
}
|
||||
if shard.ConnMaxLifetime < 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s.conn_max_lifetime_seconds must be non-negative", field))
|
||||
}
|
||||
}
|
||||
if useHashSlots {
|
||||
for idx, shard := range section.MySQLShards {
|
||||
if strings.TrimSpace(shard.HashSlots) == "" {
|
||||
*errs = append(*errs, fmt.Errorf("profile_store.mysql_shards[%d].hash_slots is required when any shard uses slot routing", idx))
|
||||
}
|
||||
}
|
||||
for slot, owner := range hashSlotOwners {
|
||||
if owner == "" {
|
||||
*errs = append(*errs, fmt.Errorf("profile_store.mysql_shards hash slot %d is not assigned", slot))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type profileHashSlots struct {
|
||||
start int
|
||||
end int
|
||||
}
|
||||
|
||||
func parseHashSlotRanges(value string) ([]profileHashSlots, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("is required")
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
out := make([]profileHashSlots, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
return nil, fmt.Errorf("contains empty range")
|
||||
}
|
||||
bounds := strings.Split(part, "-")
|
||||
if len(bounds) > 2 {
|
||||
return nil, fmt.Errorf("range %q is invalid", part)
|
||||
}
|
||||
start, err := strconv.Atoi(strings.TrimSpace(bounds[0]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("range %q has invalid start", part)
|
||||
}
|
||||
end := start
|
||||
if len(bounds) == 2 {
|
||||
end, err = strconv.Atoi(strings.TrimSpace(bounds[1]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("range %q has invalid end", part)
|
||||
}
|
||||
}
|
||||
if start < 0 || end < start || end >= 1024 {
|
||||
return nil, fmt.Errorf("range %q must be within 0-1023", part)
|
||||
}
|
||||
out = append(out, profileHashSlots{start: start, end: end})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrConfigProviderRequired = errors.New("config provider is required")
|
||||
ErrConfigKeyRequired = errors.New("config key is required")
|
||||
ErrConfigSnapshotNotFound = errors.New("config snapshot not found")
|
||||
)
|
||||
|
||||
type Snapshot struct {
|
||||
Key string `json:"key"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
LoadSnapshot(context.Context, string) (Snapshot, bool, error)
|
||||
}
|
||||
|
||||
type WatchProvider interface {
|
||||
Provider
|
||||
WatchSnapshots(context.Context, string) (<-chan Snapshot, <-chan error)
|
||||
}
|
||||
|
||||
type ProviderFunc func(context.Context, string) (Snapshot, bool, error)
|
||||
|
||||
func (f ProviderFunc) LoadSnapshot(ctx context.Context, key string) (Snapshot, bool, error) {
|
||||
if f == nil {
|
||||
return Snapshot{}, false, ErrConfigProviderRequired
|
||||
}
|
||||
return f(ctx, key)
|
||||
}
|
||||
|
||||
func LoadRequiredSnapshot(ctx context.Context, provider Provider, key string) (Snapshot, error) {
|
||||
if provider == nil {
|
||||
return Snapshot{}, ErrConfigProviderRequired
|
||||
}
|
||||
if strings.TrimSpace(key) == "" {
|
||||
return Snapshot{}, ErrConfigKeyRequired
|
||||
}
|
||||
snapshot, ok, err := provider.LoadSnapshot(ctx, key)
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
if !ok {
|
||||
return Snapshot{}, ErrConfigSnapshotNotFound
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package config
|
||||
|
||||
func (c ServiceConfig) Redacted() ServiceConfig {
|
||||
redacted := c
|
||||
if redacted.Admin.Token != "" {
|
||||
redacted.Admin.Token = "[redacted]"
|
||||
}
|
||||
if len(redacted.Admin.Tokens) > 0 {
|
||||
redacted.Admin.Tokens = append([]AdminTokenSection(nil), redacted.Admin.Tokens...)
|
||||
for idx := range redacted.Admin.Tokens {
|
||||
if len(redacted.Admin.Tokens[idx].Scopes) > 0 {
|
||||
redacted.Admin.Tokens[idx].Scopes = append([]string(nil), redacted.Admin.Tokens[idx].Scopes...)
|
||||
}
|
||||
if redacted.Admin.Tokens[idx].Token != "" {
|
||||
redacted.Admin.Tokens[idx].Token = "[redacted]"
|
||||
}
|
||||
}
|
||||
}
|
||||
if redacted.Gateway.AuthToken != "" {
|
||||
redacted.Gateway.AuthToken = "[redacted]"
|
||||
}
|
||||
if redacted.Gateway.LoginToken != "" {
|
||||
redacted.Gateway.LoginToken = "[redacted]"
|
||||
}
|
||||
if redacted.Gateway.SessionKeyMasterSecret != "" {
|
||||
redacted.Gateway.SessionKeyMasterSecret = "[redacted]"
|
||||
}
|
||||
if redacted.ProfileStore.StorePassword != "" {
|
||||
redacted.ProfileStore.StorePassword = "[redacted]"
|
||||
}
|
||||
if redacted.ProfileStore.SummaryStorePassword != "" {
|
||||
redacted.ProfileStore.SummaryStorePassword = "[redacted]"
|
||||
}
|
||||
if redacted.ProfileStore.MySQLDSN != "" {
|
||||
redacted.ProfileStore.MySQLDSN = "[redacted]"
|
||||
}
|
||||
if len(redacted.ProfileStore.MySQLShards) > 0 {
|
||||
redacted.ProfileStore.MySQLShards = append([]ProfileMySQLShard(nil), redacted.ProfileStore.MySQLShards...)
|
||||
for idx := range redacted.ProfileStore.MySQLShards {
|
||||
if redacted.ProfileStore.MySQLShards[idx].DSN != "" {
|
||||
redacted.ProfileStore.MySQLShards[idx].DSN = "[redacted]"
|
||||
}
|
||||
}
|
||||
}
|
||||
if redacted.Idempotency.RedisPassword != "" {
|
||||
redacted.Idempotency.RedisPassword = "[redacted]"
|
||||
}
|
||||
if redacted.Idempotency.MySQLDSN != "" {
|
||||
redacted.Idempotency.MySQLDSN = "[redacted]"
|
||||
}
|
||||
if redacted.Registry.RedisPassword != "" {
|
||||
redacted.Registry.RedisPassword = "[redacted]"
|
||||
}
|
||||
if redacted.Bus.RedisPassword != "" {
|
||||
redacted.Bus.RedisPassword = "[redacted]"
|
||||
}
|
||||
if redacted.Bus.NATSToken != "" {
|
||||
redacted.Bus.NATSToken = "[redacted]"
|
||||
}
|
||||
if redacted.Bus.NATSPassword != "" {
|
||||
redacted.Bus.NATSPassword = "[redacted]"
|
||||
}
|
||||
if redacted.Bus.NATSCredentials != "" {
|
||||
redacted.Bus.NATSCredentials = "[redacted]"
|
||||
}
|
||||
if redacted.EventLog.MySQLDSN != "" {
|
||||
redacted.EventLog.MySQLDSN = "[redacted]"
|
||||
}
|
||||
if redacted.AccountCenter.MySQLDSN != "" {
|
||||
redacted.AccountCenter.MySQLDSN = "[redacted]"
|
||||
}
|
||||
if redacted.EventLog.PublishHTTPURL != "" {
|
||||
redacted.EventLog.PublishHTTPURL = "[redacted]"
|
||||
}
|
||||
if len(redacted.EventLog.PublishHTTPHeaders) > 0 {
|
||||
headers := make(map[string]string, len(redacted.EventLog.PublishHTTPHeaders))
|
||||
for key := range redacted.EventLog.PublishHTTPHeaders {
|
||||
headers[key] = "[redacted]"
|
||||
}
|
||||
redacted.EventLog.PublishHTTPHeaders = headers
|
||||
}
|
||||
if redacted.Cache.RedisPassword != "" {
|
||||
redacted.Cache.RedisPassword = "[redacted]"
|
||||
}
|
||||
if redacted.Lock.RedisPassword != "" {
|
||||
redacted.Lock.RedisPassword = "[redacted]"
|
||||
}
|
||||
if redacted.Presence.RedisPassword != "" {
|
||||
redacted.Presence.RedisPassword = "[redacted]"
|
||||
}
|
||||
if redacted.StorageObject.MySQLDSN != "" {
|
||||
redacted.StorageObject.MySQLDSN = "[redacted]"
|
||||
}
|
||||
return redacted
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
package config
|
||||
|
||||
type ServiceConfig struct {
|
||||
Service ServiceSection `toml:"service"`
|
||||
Cluster ClusterSection `toml:"cluster"`
|
||||
Admin AdminSection `toml:"admin"`
|
||||
Metrics MetricsSection `toml:"metrics"`
|
||||
Tracing TracingSection `toml:"tracing"`
|
||||
OTel OTelSection `toml:"otel"`
|
||||
Debug DebugSection `toml:"debug"`
|
||||
DataTables DataTablesSection `toml:"data_tables"`
|
||||
I18N I18NSection `toml:"i18n"`
|
||||
PVF PVFSection `toml:"pvf"`
|
||||
Audit AuditSection `toml:"audit"`
|
||||
LogicLog LogicLogSection `toml:"logic_log"`
|
||||
BILog BILogSection `toml:"bi_log"`
|
||||
EventLog EventLogSection `toml:"eventlog"`
|
||||
AccountCenter AccountCenterSection `toml:"account_center"`
|
||||
StorageObject StorageObjectSection `toml:"storage_object"`
|
||||
BattleAgent BattleAgentSection `toml:"battle_agent"`
|
||||
Reload ReloadSection `toml:"reload"`
|
||||
HotUpdate HotUpdateSection `toml:"hot_update"`
|
||||
StateSync StateSyncSection `toml:"state_sync"`
|
||||
ServerGroup ServerGroupSection `toml:"server_group"`
|
||||
Topology TopologySection `toml:"topology"`
|
||||
Discovery DiscoverySection `toml:"discovery"`
|
||||
RPC RPCSection `toml:"rpc"`
|
||||
RateLimit RateLimitSection `toml:"rate_limit"`
|
||||
Idempotency IdempotencySection `toml:"idempotency"`
|
||||
Registry RegistrySection `toml:"registry"`
|
||||
Bus BusSection `toml:"bus"`
|
||||
Cache CacheSection `toml:"cache"`
|
||||
Lock LockSection `toml:"lock"`
|
||||
Presence PresenceSection `toml:"presence"`
|
||||
Worker WorkerSection `toml:"worker"`
|
||||
ProfileStore ProfileStoreSection `toml:"profile_store"`
|
||||
Gateway GatewaySection `toml:"gateway"`
|
||||
}
|
||||
|
||||
type ServiceSection struct {
|
||||
Name string `toml:"name"`
|
||||
NodeID string `toml:"node_id"`
|
||||
Environment string `toml:"environment"`
|
||||
PublicAddr string `toml:"public_addr"`
|
||||
PrivateAddr string `toml:"private_addr"`
|
||||
StopTimeoutSeconds int `toml:"stop_timeout_seconds"`
|
||||
StopInitialGraceSeconds int `toml:"stop_initial_grace_seconds"`
|
||||
StopProbeSeconds int `toml:"stop_probe_seconds"`
|
||||
StopStableSeconds int `toml:"stop_stable_seconds"`
|
||||
}
|
||||
|
||||
type ClusterSection struct {
|
||||
ID string `toml:"id"`
|
||||
GID int64 `toml:"gid"`
|
||||
VirtualGID int64 `toml:"virtual_gid"`
|
||||
ShardID int64 `toml:"shard_id"`
|
||||
Zone string `toml:"zone"`
|
||||
Version string `toml:"version"`
|
||||
DataVersion string `toml:"data_version"`
|
||||
Maintaining bool `toml:"maintaining"`
|
||||
HeartbeatIntervalSeconds int `toml:"heartbeat_interval_seconds"`
|
||||
}
|
||||
|
||||
type AdminSection struct {
|
||||
Listen string `toml:"listen"`
|
||||
Token string `toml:"token"`
|
||||
RBACEnabled bool `toml:"rbac_enabled"`
|
||||
DangerousConfirm bool `toml:"dangerous_confirm"`
|
||||
ReadTimeoutSeconds int `toml:"read_timeout_seconds"`
|
||||
ReadHeaderTimeoutSeconds int `toml:"read_header_timeout_seconds"`
|
||||
WriteTimeoutSeconds int `toml:"write_timeout_seconds"`
|
||||
IdleTimeoutSeconds int `toml:"idle_timeout_seconds"`
|
||||
Tokens []AdminTokenSection `toml:"tokens"`
|
||||
}
|
||||
|
||||
type AdminTokenSection struct {
|
||||
Name string `toml:"name"`
|
||||
Token string `toml:"token"`
|
||||
Role string `toml:"role"`
|
||||
Scopes []string `toml:"scopes"`
|
||||
}
|
||||
|
||||
type MetricsSection struct {
|
||||
PrometheusEnabled bool `toml:"prometheus_enabled"`
|
||||
RequireAdminToken bool `toml:"require_admin_token"`
|
||||
}
|
||||
|
||||
type TracingSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MaxSpans int `toml:"max_spans"`
|
||||
}
|
||||
|
||||
type OTelSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Endpoint string `toml:"endpoint"`
|
||||
Insecure bool `toml:"insecure"`
|
||||
ServiceName string `toml:"service_name"`
|
||||
SampleRatio float64 `toml:"sample_ratio"`
|
||||
BatchTimeoutSeconds int `toml:"batch_timeout_seconds"`
|
||||
}
|
||||
|
||||
type DebugSection struct {
|
||||
PprofEnabled bool `toml:"pprof_enabled"`
|
||||
}
|
||||
|
||||
type DataTablesSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Directory string `toml:"directory"`
|
||||
Version string `toml:"version"`
|
||||
}
|
||||
|
||||
type I18NSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Directory string `toml:"directory"`
|
||||
DefaultLanguage string `toml:"default_language"`
|
||||
Version string `toml:"version"`
|
||||
}
|
||||
|
||||
type AuditSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Kind string `toml:"kind"`
|
||||
FilePath string `toml:"file_path"`
|
||||
MemoryLimit int `toml:"memory_limit"`
|
||||
}
|
||||
|
||||
type LogicLogSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Kind string `toml:"kind"`
|
||||
FilePath string `toml:"file_path"`
|
||||
ReasonsPath string `toml:"reasons_path"`
|
||||
MemoryLimit int `toml:"memory_limit"`
|
||||
}
|
||||
|
||||
type BILogSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Kind string `toml:"kind"`
|
||||
FilePath string `toml:"file_path"`
|
||||
SchemaPath string `toml:"schema_path"`
|
||||
MemoryLimit int `toml:"memory_limit"`
|
||||
}
|
||||
|
||||
type EventLogSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
StoreKind string `toml:"store_kind"`
|
||||
MySQLDSN string `toml:"mysql_dsn"`
|
||||
MySQLTable string `toml:"mysql_table"`
|
||||
MySQLMaxOpenConns int `toml:"mysql_max_open_conns"`
|
||||
MySQLMaxIdleConns int `toml:"mysql_max_idle_conns"`
|
||||
MySQLConnMaxLifetime int `toml:"mysql_conn_max_lifetime_seconds"`
|
||||
MySQLEnsureSchema bool `toml:"mysql_ensure_schema"`
|
||||
AdminPrefix string `toml:"admin_prefix"`
|
||||
Strict bool `toml:"strict"`
|
||||
PublishEnabled bool `toml:"publish_enabled"`
|
||||
PublishIntervalSeconds int `toml:"publish_interval_seconds"`
|
||||
PublishBatchSize int `toml:"publish_batch_size"`
|
||||
PublishRetrySeconds int `toml:"publish_retry_seconds"`
|
||||
PublishMaxAttempts int `toml:"publish_max_attempts"`
|
||||
PublishKind string `toml:"publish_kind"`
|
||||
PublishHTTPURL string `toml:"publish_http_url"`
|
||||
PublishHTTPMethod string `toml:"publish_http_method"`
|
||||
PublishHTTPTimeout int `toml:"publish_http_timeout_seconds"`
|
||||
PublishHTTPPayload string `toml:"publish_http_payload"`
|
||||
PublishHTTPHeaders map[string]string `toml:"publish_http_headers"`
|
||||
}
|
||||
|
||||
type AccountCenterSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
StoreKind string `toml:"store_kind"`
|
||||
MySQLDSN string `toml:"mysql_dsn"`
|
||||
MySQLTable string `toml:"mysql_table"`
|
||||
MySQLEnsureSchema bool `toml:"mysql_ensure_schema"`
|
||||
AdminPrefix string `toml:"admin_prefix"`
|
||||
}
|
||||
|
||||
type StorageObjectSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
StoreKind string `toml:"store_kind"`
|
||||
MySQLDSN string `toml:"mysql_dsn"`
|
||||
MySQLTable string `toml:"mysql_table"`
|
||||
MySQLEnsureSchema bool `toml:"mysql_ensure_schema"`
|
||||
AdminPrefix string `toml:"admin_prefix"`
|
||||
}
|
||||
|
||||
type BattleAgentSection struct {
|
||||
SettlementOutboxKind string `toml:"settlement_outbox_kind"`
|
||||
SettlementOutboxStream string `toml:"settlement_outbox_stream"`
|
||||
SettlementOutboxType string `toml:"settlement_outbox_type"`
|
||||
}
|
||||
|
||||
type ReloadSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
PollIntervalSeconds int `toml:"poll_interval_seconds"`
|
||||
ApplyTimeoutSeconds int `toml:"apply_timeout_seconds"`
|
||||
}
|
||||
|
||||
type HotUpdateSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Target string `toml:"target"`
|
||||
ControlKind string `toml:"control_kind"`
|
||||
SourceKind string `toml:"source_kind"`
|
||||
ApplierKind string `toml:"applier_kind"`
|
||||
Workspace string `toml:"workspace"`
|
||||
ApplyTimeoutSeconds int `toml:"apply_timeout_seconds"`
|
||||
SigningKeyEnv string `toml:"signing_key_env"`
|
||||
RequireSignature bool `toml:"require_signature"`
|
||||
NativePatchAllowedTargets []string `toml:"native_patch_allowed_targets"`
|
||||
PatchOldSymbols []string `toml:"native_patch_allowed_old_symbols"`
|
||||
NativePatchRequireBuildID bool `toml:"native_patch_require_build_id"`
|
||||
PatchRequireActor bool `toml:"native_patch_require_requested_by"`
|
||||
NativePatchRequireReason bool `toml:"native_patch_require_reason"`
|
||||
NativePatchMinReasonLength int `toml:"native_patch_min_reason_length"`
|
||||
NativePatchMaxSymbols int `toml:"native_patch_max_symbols"`
|
||||
NativePatchMaxLiveSeconds int `toml:"native_patch_max_live_seconds"`
|
||||
}
|
||||
|
||||
type StateSyncSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Kind string `toml:"kind"`
|
||||
Namespace string `toml:"namespace"`
|
||||
TTLSeconds int `toml:"ttl_seconds"`
|
||||
CleanupIntervalSeconds int `toml:"cleanup_interval_seconds"`
|
||||
MaxPayloadBytes int `toml:"max_payload_bytes"`
|
||||
}
|
||||
|
||||
type ServerGroupSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
StoreKind string `toml:"store_kind"`
|
||||
PlanFile string `toml:"plan_file"`
|
||||
MergeArchiveDir string `toml:"merge_archive_dir"`
|
||||
AdminPrefix string `toml:"admin_prefix"`
|
||||
InZoneDelaySeconds int `toml:"in_zone_delay_seconds"`
|
||||
NoticeLeadSeconds int `toml:"notice_lead_seconds"`
|
||||
}
|
||||
|
||||
type TopologySection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Services []string `toml:"services"`
|
||||
GatewayService string `toml:"gateway_service"`
|
||||
GatewayBackendService string `toml:"gateway_backend_service"`
|
||||
RejectMaintaining bool `toml:"reject_maintaining"`
|
||||
RefreshIntervalSeconds int `toml:"refresh_interval_seconds"`
|
||||
}
|
||||
|
||||
type DiscoverySection struct {
|
||||
CacheTTLSeconds int `toml:"cache_ttl_seconds"`
|
||||
FailureTTLSeconds int `toml:"failure_ttl_seconds"`
|
||||
FailureThreshold int `toml:"failure_threshold"`
|
||||
AllowMaintaining bool `toml:"allow_maintaining"`
|
||||
Strategy string `toml:"strategy"`
|
||||
}
|
||||
|
||||
type RPCSection struct {
|
||||
CallTimeoutSeconds int `toml:"call_timeout_seconds"`
|
||||
MaxPending int `toml:"max_pending"`
|
||||
MaxPayloadBytes int `toml:"max_payload_bytes"`
|
||||
}
|
||||
|
||||
type RateLimitSection struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Algorithm string `toml:"algorithm"`
|
||||
WindowSeconds int `toml:"window_seconds"`
|
||||
MaxRequests int `toml:"max_requests"`
|
||||
Rules string `toml:"rules"`
|
||||
CleanupIntervalSeconds int `toml:"cleanup_interval_seconds"`
|
||||
TrustedHeader string `toml:"trusted_header"`
|
||||
}
|
||||
|
||||
type IdempotencySection struct {
|
||||
Kind string `toml:"kind"`
|
||||
TTLSeconds int `toml:"ttl_seconds"`
|
||||
KeyPrefix string `toml:"key_prefix"`
|
||||
RedisAddress string `toml:"redis_address"`
|
||||
RedisPassword string `toml:"redis_password"`
|
||||
RedisDB int `toml:"redis_db"`
|
||||
RedisPoolSize int `toml:"redis_pool_size"`
|
||||
RedisTimeoutSeconds int `toml:"redis_timeout_seconds"`
|
||||
RedisConnectTimeoutSecs int `toml:"redis_connect_timeout_seconds"`
|
||||
RedisReadTimeoutSecs int `toml:"redis_read_timeout_seconds"`
|
||||
RedisWriteTimeoutSecs int `toml:"redis_write_timeout_seconds"`
|
||||
MySQLDSN string `toml:"mysql_dsn"`
|
||||
MySQLMaxOpenConns int `toml:"mysql_max_open_conns"`
|
||||
MySQLMaxIdleConns int `toml:"mysql_max_idle_conns"`
|
||||
MySQLConnMaxLifetimeSec int `toml:"mysql_conn_max_lifetime_seconds"`
|
||||
}
|
||||
|
||||
type RegistrySection struct {
|
||||
Kind string `toml:"kind"`
|
||||
Namespace string `toml:"namespace"`
|
||||
Endpoints []string `toml:"endpoints"`
|
||||
LeaseTTL int64 `toml:"lease_ttl"`
|
||||
RedisPassword string `toml:"redis_password"`
|
||||
RedisDB int `toml:"redis_db"`
|
||||
}
|
||||
|
||||
type BusSection struct {
|
||||
Kind string `toml:"kind"`
|
||||
Namespace string `toml:"namespace"`
|
||||
Endpoints []string `toml:"endpoints"`
|
||||
RedisUsername string `toml:"redis_username"`
|
||||
RedisPassword string `toml:"redis_password"`
|
||||
RedisDB int `toml:"redis_db"`
|
||||
RedisTLSEnabled bool `toml:"redis_tls_enabled"`
|
||||
RedisTLSServerName string `toml:"redis_tls_server_name"`
|
||||
NATSToken string `toml:"nats_token"`
|
||||
NATSUsername string `toml:"nats_username"`
|
||||
NATSPassword string `toml:"nats_password"`
|
||||
NATSCredentials string `toml:"nats_credentials"`
|
||||
NATSTLSEnabled bool `toml:"nats_tls_enabled"`
|
||||
NATSTLSServerName string `toml:"nats_tls_server_name"`
|
||||
NATSCAFile string `toml:"nats_ca_file"`
|
||||
NATSCertFile string `toml:"nats_cert_file"`
|
||||
NATSKeyFile string `toml:"nats_key_file"`
|
||||
NATSName string `toml:"nats_name"`
|
||||
NATSTimeoutSeconds int `toml:"nats_timeout_seconds"`
|
||||
NATSPingIntervalSeconds int `toml:"nats_ping_interval_seconds"`
|
||||
NATSMaxPingsOutstanding int `toml:"nats_max_pings_outstanding"`
|
||||
NATSMaxReconnects int `toml:"nats_max_reconnects"`
|
||||
NATSReconnectWaitSeconds int `toml:"nats_reconnect_wait_seconds"`
|
||||
NATSWireEncoding string `toml:"nats_wire_encoding"`
|
||||
}
|
||||
|
||||
type CacheSection struct {
|
||||
Kind string `toml:"kind"`
|
||||
KeyPrefix string `toml:"key_prefix"`
|
||||
DefaultTTLSeconds int `toml:"default_ttl_seconds"`
|
||||
MaxEntries int `toml:"max_entries"`
|
||||
RedisAddress string `toml:"redis_address"`
|
||||
RedisPassword string `toml:"redis_password"`
|
||||
RedisDB int `toml:"redis_db"`
|
||||
RedisPoolSize int `toml:"redis_pool_size"`
|
||||
RedisTimeoutSeconds int `toml:"redis_timeout_seconds"`
|
||||
RedisConnectTimeout int `toml:"redis_connect_timeout_seconds"`
|
||||
RedisReadTimeout int `toml:"redis_read_timeout_seconds"`
|
||||
RedisWriteTimeout int `toml:"redis_write_timeout_seconds"`
|
||||
}
|
||||
|
||||
type LockSection struct {
|
||||
Kind string `toml:"kind"`
|
||||
KeyPrefix string `toml:"key_prefix"`
|
||||
TTLSeconds int `toml:"ttl_seconds"`
|
||||
RedisAddress string `toml:"redis_address"`
|
||||
RedisPassword string `toml:"redis_password"`
|
||||
RedisDB int `toml:"redis_db"`
|
||||
RedisPoolSize int `toml:"redis_pool_size"`
|
||||
RedisTimeoutSeconds int `toml:"redis_timeout_seconds"`
|
||||
RedisConnectTimeout int `toml:"redis_connect_timeout_seconds"`
|
||||
RedisReadTimeout int `toml:"redis_read_timeout_seconds"`
|
||||
RedisWriteTimeout int `toml:"redis_write_timeout_seconds"`
|
||||
}
|
||||
|
||||
type PresenceSection struct {
|
||||
Kind string `toml:"kind"`
|
||||
KeyPrefix string `toml:"key_prefix"`
|
||||
TTLSeconds int `toml:"ttl_seconds"`
|
||||
RedisAddress string `toml:"redis_address"`
|
||||
RedisPassword string `toml:"redis_password"`
|
||||
RedisDB int `toml:"redis_db"`
|
||||
RedisPoolSize int `toml:"redis_pool_size"`
|
||||
RedisTimeoutSeconds int `toml:"redis_timeout_seconds"`
|
||||
RedisConnectTimeout int `toml:"redis_connect_timeout_seconds"`
|
||||
RedisReadTimeout int `toml:"redis_read_timeout_seconds"`
|
||||
RedisWriteTimeout int `toml:"redis_write_timeout_seconds"`
|
||||
}
|
||||
|
||||
type WorkerSection struct {
|
||||
Size int `toml:"size"`
|
||||
Queue int `toml:"queue"`
|
||||
}
|
||||
|
||||
type ProfileStoreSection struct {
|
||||
StoreKind string `toml:"store_kind"`
|
||||
StoreDirectory string `toml:"store_directory"`
|
||||
StoreAddress string `toml:"store_address"`
|
||||
StorePassword string `toml:"store_password"`
|
||||
StoreDB int `toml:"store_db"`
|
||||
StoreKeyPrefix string `toml:"store_key_prefix"`
|
||||
StorePoolSize int `toml:"store_pool_size"`
|
||||
StoreTimeoutSeconds int `toml:"store_timeout_seconds"`
|
||||
StoreConnectTimeout int `toml:"store_connect_timeout_seconds"`
|
||||
StoreReadTimeout int `toml:"store_read_timeout_seconds"`
|
||||
StoreWriteTimeout int `toml:"store_write_timeout_seconds"`
|
||||
StoreTTLSeconds int `toml:"store_ttl_seconds"`
|
||||
MySQLDSN string `toml:"mysql_dsn"`
|
||||
MySQLMaxOpenConns int `toml:"mysql_max_open_conns"`
|
||||
MySQLMaxIdleConns int `toml:"mysql_max_idle_conns"`
|
||||
MySQLConnMaxLifetime int `toml:"mysql_conn_max_lifetime_seconds"`
|
||||
MySQLShardingEnabled bool `toml:"mysql_sharding_enabled"`
|
||||
MySQLShards []ProfileMySQLShard `toml:"mysql_shards"`
|
||||
SummaryStoreKind string `toml:"summary_store_kind"`
|
||||
SummaryStoreAddress string `toml:"summary_store_address"`
|
||||
SummaryStorePassword string `toml:"summary_store_password"`
|
||||
SummaryStoreDB int `toml:"summary_store_db"`
|
||||
SummaryStoreKeyPrefix string `toml:"summary_store_key_prefix"`
|
||||
SummaryStorePoolSize int `toml:"summary_store_pool_size"`
|
||||
SummaryStoreTimeout int `toml:"summary_store_timeout_seconds"`
|
||||
SummaryStoreConnectTimeout int `toml:"summary_store_connect_timeout_seconds"`
|
||||
SummaryStoreReadTimeout int `toml:"summary_store_read_timeout_seconds"`
|
||||
SummaryStoreWriteTimeout int `toml:"summary_store_write_timeout_seconds"`
|
||||
SummaryStoreTTL int `toml:"summary_store_ttl_seconds"`
|
||||
SaveMode string `toml:"save_mode"`
|
||||
AsyncFlushIntervalSeconds int `toml:"async_flush_interval_seconds"`
|
||||
AsyncMaxPending int `toml:"async_max_pending"`
|
||||
AsyncRetryBackoffSeconds int `toml:"async_retry_backoff_seconds"`
|
||||
AsyncMaxRetries int `toml:"async_max_retries"`
|
||||
AsyncAutoExpireSeconds int `toml:"async_auto_expire_seconds"`
|
||||
AsyncDeadLetterLimit int `toml:"async_dead_letter_limit"`
|
||||
AsyncDeadLetterDirectory string `toml:"async_dead_letter_directory"`
|
||||
}
|
||||
|
||||
type ProfileMySQLShard struct {
|
||||
ID string `toml:"id"`
|
||||
DSN string `toml:"dsn"`
|
||||
TableName string `toml:"table_name"`
|
||||
TablePrefix string `toml:"table_prefix"`
|
||||
HashSlots string `toml:"hash_slots"`
|
||||
MaxOpenConns int `toml:"max_open_conns"`
|
||||
MaxIdleConns int `toml:"max_idle_conns"`
|
||||
ConnMaxLifetime int `toml:"conn_max_lifetime_seconds"`
|
||||
}
|
||||
|
||||
type GatewaySection struct {
|
||||
AuthListen string `toml:"auth_listen"`
|
||||
AuthMode string `toml:"auth_mode"`
|
||||
SharedLoginTokens bool `toml:"shared_login_tokens"`
|
||||
RegistryGateSelection bool `toml:"registry_gate_selection"`
|
||||
RemoteGateSelection bool `toml:"remote_gate_selection"`
|
||||
GateRouteFeature string `toml:"gate_route_feature"`
|
||||
GateService string `toml:"gate_service"`
|
||||
TCPListen string `toml:"tcp_listen"`
|
||||
TCPProxyProtocolEnabled bool `toml:"tcp_proxy_protocol_enabled"`
|
||||
TCPProxyTrustedCIDRs []string `toml:"tcp_proxy_trusted_cidrs"`
|
||||
TCPProxyHeaderSec int `toml:"tcp_proxy_header_timeout_seconds"`
|
||||
KCPEnabled bool `toml:"kcp_enabled"`
|
||||
KCPListen string `toml:"kcp_listen"`
|
||||
KCPNoDelay int `toml:"kcp_nodelay"`
|
||||
KCPInterval int `toml:"kcp_interval"`
|
||||
KCPResend int `toml:"kcp_resend"`
|
||||
KCPNoCongestion int `toml:"kcp_no_congestion"`
|
||||
KCPWindowSize int `toml:"kcp_window_size"`
|
||||
KCPDataShards int `toml:"kcp_data_shards"`
|
||||
KCPParityShards int `toml:"kcp_parity_shards"`
|
||||
QUICEnabled bool `toml:"quic_enabled"`
|
||||
QUICListen string `toml:"quic_listen"`
|
||||
QUICCertFile string `toml:"quic_cert_file"`
|
||||
QUICKeyFile string `toml:"quic_key_file"`
|
||||
QUICHandshakeSec int `toml:"quic_handshake_timeout_seconds"`
|
||||
QUICMaxIdleTimeoutSeconds int `toml:"quic_max_idle_timeout_seconds"`
|
||||
QUICKeepAliveSeconds int `toml:"quic_keep_alive_seconds"`
|
||||
QUICMaxIncomingStreams int64 `toml:"quic_max_incoming_streams"`
|
||||
WebSocketEnabled bool `toml:"websocket_enabled"`
|
||||
WebSocketListen string `toml:"websocket_listen"`
|
||||
WebSocketPath string `toml:"websocket_path"`
|
||||
WebSocketMaxPayloadBytes int `toml:"websocket_max_payload_bytes"`
|
||||
WebSocketHeartbeatSeconds int `toml:"websocket_heartbeat_seconds"`
|
||||
ChatListen string `toml:"chat_listen"`
|
||||
BackendService string `toml:"backend_service"`
|
||||
BackendPollIntervalSeconds int `toml:"backend_poll_interval_seconds"`
|
||||
RejectMaintainingBackends bool `toml:"reject_maintaining_backends"`
|
||||
MaxConnections int `toml:"max_connections"`
|
||||
ConnectionRateLimitEnabled bool `toml:"connection_rate_limit_enabled"`
|
||||
ConnRateWindowSec int `toml:"connection_rate_limit_window_seconds"`
|
||||
ConnectionRateLimitMax int `toml:"connection_rate_limit_max"`
|
||||
ConnRateMaxEntries int `toml:"connection_rate_limit_max_entries"`
|
||||
ConnRateIPv4Prefix int `toml:"connection_rate_limit_ipv4_prefix"`
|
||||
ConnRateIPv6Prefix int `toml:"connection_rate_limit_ipv6_prefix"`
|
||||
SessionIdleTimeoutSeconds int `toml:"session_idle_timeout_seconds"`
|
||||
SessionSweepSec int `toml:"session_sweep_interval_seconds"`
|
||||
SessionHookTimeoutSeconds int `toml:"session_hook_timeout_seconds"`
|
||||
SessionKeyMasterSecret string `toml:"session_key_master_secret"`
|
||||
AccountID string `toml:"account_id"`
|
||||
AccountNumID int32 `toml:"account_num_id"`
|
||||
AuthToken string `toml:"auth_token"`
|
||||
LoginToken string `toml:"login_token"`
|
||||
ServerVersion string `toml:"server_version"`
|
||||
ShardID int32 `toml:"shard_id"`
|
||||
ShardName string `toml:"shard_name"`
|
||||
ShardDisplay string `toml:"shard_display"`
|
||||
ShardState int32 `toml:"shard_state"`
|
||||
Recommend int32 `toml:"recommend"`
|
||||
RecommendLang string `toml:"recommend_lang"`
|
||||
VirtualGID int32 `toml:"virtual_gid"`
|
||||
ProductIDs []string `toml:"product_ids"`
|
||||
HasRole bool `toml:"has_role"`
|
||||
SendCreatePush bool `toml:"send_create_profile_push"`
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validateAdminTokens(errs *[]error, admin AdminSection) {
|
||||
if strings.ContainsAny(admin.Token, "\r\n") {
|
||||
*errs = append(*errs, fmt.Errorf("admin.token cannot contain line breaks"))
|
||||
}
|
||||
seen := map[string]string{}
|
||||
if admin.Token != "" {
|
||||
seen[admin.Token] = "admin.token"
|
||||
}
|
||||
for idx, token := range admin.Tokens {
|
||||
prefix := fmt.Sprintf("admin.tokens[%d]", idx)
|
||||
requireString(errs, prefix+".name", token.Name)
|
||||
requireString(errs, prefix+".token", token.Token)
|
||||
requireOneOf(errs, prefix+".role", token.Role, "admin", "operator", "viewer", "auditor")
|
||||
if strings.ContainsAny(token.Token, "\r\n") {
|
||||
*errs = append(*errs, fmt.Errorf("%s.token cannot contain line breaks", prefix))
|
||||
}
|
||||
if token.Token != "" {
|
||||
if first, ok := seen[token.Token]; ok {
|
||||
*errs = append(*errs, fmt.Errorf("%s.token duplicates %s", prefix, first))
|
||||
} else {
|
||||
seen[token.Token] = prefix + ".token"
|
||||
}
|
||||
}
|
||||
for scopeIdx, scope := range token.Scopes {
|
||||
if scope == "" {
|
||||
*errs = append(*errs, fmt.Errorf("%s.scopes[%d] is required", prefix, scopeIdx))
|
||||
continue
|
||||
}
|
||||
for _, r := range scope {
|
||||
if isAdminScopeRune(r) {
|
||||
continue
|
||||
}
|
||||
*errs = append(*errs, fmt.Errorf("%s.scopes[%d] %q can only contain letters, digits, dot, dash, underscore, colon, or star", prefix, scopeIdx, scope))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isAdminScopeRune(r rune) bool {
|
||||
return r == '.' || r == '-' || r == '_' || r == ':' || r == '*' ||
|
||||
(r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9')
|
||||
}
|
||||
|
||||
func (c ServiceConfig) Validate() error {
|
||||
var errs []error
|
||||
productionLike := isProdLikeEnv(c.Service.Environment)
|
||||
requireSafeName(&errs, "service.name", c.Service.Name)
|
||||
requireSafeName(&errs, "service.node_id", c.Service.NodeID)
|
||||
requireString(&errs, "service.environment", c.Service.Environment)
|
||||
requirePositiveInt(&errs, "service.stop_timeout_seconds", c.Service.StopTimeoutSeconds)
|
||||
requireNonNegInt(&errs, "service.stop_initial_grace_seconds", c.Service.StopInitialGraceSeconds)
|
||||
requirePositiveInt(&errs, "service.stop_probe_seconds", c.Service.StopProbeSeconds)
|
||||
requirePositiveInt(&errs, "service.stop_stable_seconds", c.Service.StopStableSeconds)
|
||||
if productionLike {
|
||||
validateProdSafety(&errs, c)
|
||||
}
|
||||
|
||||
requireSafeName(&errs, "cluster.id", c.Cluster.ID)
|
||||
requireNonNegInt64(&errs, "cluster.gid", c.Cluster.GID)
|
||||
requireNonNegInt64(&errs, "cluster.virtual_gid", c.Cluster.VirtualGID)
|
||||
requireNonNegInt64(&errs, "cluster.shard_id", c.Cluster.ShardID)
|
||||
requireString(&errs, "cluster.zone", c.Cluster.Zone)
|
||||
requireString(&errs, "cluster.version", c.Cluster.Version)
|
||||
requireString(&errs, "cluster.data_version", c.Cluster.DataVersion)
|
||||
requirePositiveInt(&errs, "cluster.heartbeat_interval_seconds", c.Cluster.HeartbeatIntervalSeconds)
|
||||
|
||||
requireListenAddress(&errs, "admin.listen", c.Admin.Listen)
|
||||
requireString(&errs, "admin.token", c.Admin.Token)
|
||||
requirePositiveInt(&errs, "admin.read_timeout_seconds", c.Admin.ReadTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "admin.read_header_timeout_seconds", c.Admin.ReadHeaderTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "admin.write_timeout_seconds", c.Admin.WriteTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "admin.idle_timeout_seconds", c.Admin.IdleTimeoutSeconds)
|
||||
if productionLike {
|
||||
if !c.Admin.RBACEnabled {
|
||||
errs = append(errs, fmt.Errorf("admin.rbac_enabled is required for production-like environments"))
|
||||
}
|
||||
if !c.Admin.DangerousConfirm {
|
||||
errs = append(errs, fmt.Errorf("admin.dangerous_confirm is required for production-like environments"))
|
||||
}
|
||||
if !c.Audit.Enabled {
|
||||
errs = append(errs, fmt.Errorf("audit.enabled is required for production-like environments"))
|
||||
}
|
||||
}
|
||||
validateAdminTokens(&errs, c.Admin)
|
||||
|
||||
if c.RateLimit.Enabled {
|
||||
if c.RateLimit.Algorithm != "window" && c.RateLimit.Algorithm != "token_bucket" {
|
||||
errs = append(errs, fmt.Errorf("rate_limit.algorithm must be window or token_bucket"))
|
||||
}
|
||||
requirePositiveInt(&errs, "rate_limit.window_seconds", c.RateLimit.WindowSeconds)
|
||||
requirePositiveInt(&errs, "rate_limit.max_requests", c.RateLimit.MaxRequests)
|
||||
requirePositiveInt(&errs, "rate_limit.cleanup_interval_seconds", c.RateLimit.CleanupIntervalSeconds)
|
||||
if c.RateLimit.TrustedHeader != "" && strings.ContainsAny(c.RateLimit.TrustedHeader, "\r\n") {
|
||||
errs = append(errs, fmt.Errorf("rate_limit.trusted_header cannot contain line breaks"))
|
||||
}
|
||||
if err := validRateRules(c.RateLimit.Rules); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
requireNonNegInt(&errs, "tracing.max_spans", c.Tracing.MaxSpans)
|
||||
if c.OTel.Enabled {
|
||||
requireString(&errs, "otel.endpoint", c.OTel.Endpoint)
|
||||
requireString(&errs, "otel.service_name", c.OTel.ServiceName)
|
||||
requirePositiveInt(&errs, "otel.batch_timeout_seconds", c.OTel.BatchTimeoutSeconds)
|
||||
if c.OTel.SampleRatio <= 0 || c.OTel.SampleRatio > 1 {
|
||||
errs = append(errs, fmt.Errorf("otel.sample_ratio must be greater than 0 and at most 1"))
|
||||
}
|
||||
}
|
||||
if c.DataTables.Enabled {
|
||||
requireString(&errs, "data_tables.directory", c.DataTables.Directory)
|
||||
requireString(&errs, "data_tables.version", c.DataTables.Version)
|
||||
}
|
||||
if c.I18N.Enabled {
|
||||
requireString(&errs, "i18n.directory", c.I18N.Directory)
|
||||
requireString(&errs, "i18n.default_language", c.I18N.DefaultLanguage)
|
||||
requireString(&errs, "i18n.version", c.I18N.Version)
|
||||
}
|
||||
c.validatePVF(&errs)
|
||||
if c.Audit.Enabled {
|
||||
requireOneOf(&errs, "audit.kind", c.Audit.Kind, "memory", "file")
|
||||
requirePositiveInt(&errs, "audit.memory_limit", c.Audit.MemoryLimit)
|
||||
if c.Audit.Kind == "file" {
|
||||
requireString(&errs, "audit.file_path", c.Audit.FilePath)
|
||||
}
|
||||
}
|
||||
if c.LogicLog.Enabled {
|
||||
requireOneOf(&errs, "logic_log.kind", c.LogicLog.Kind, "memory", "file")
|
||||
requirePositiveInt(&errs, "logic_log.memory_limit", c.LogicLog.MemoryLimit)
|
||||
requireString(&errs, "logic_log.reasons_path", c.LogicLog.ReasonsPath)
|
||||
if c.LogicLog.Kind == "file" {
|
||||
requireString(&errs, "logic_log.file_path", c.LogicLog.FilePath)
|
||||
}
|
||||
if productionLike && c.LogicLog.Kind == "memory" {
|
||||
errs = append(errs, fmt.Errorf("logic_log.kind memory is only allowed for local/dev/test environments"))
|
||||
}
|
||||
}
|
||||
if c.BILog.Enabled {
|
||||
requireOneOf(&errs, "bi_log.kind", c.BILog.Kind, "memory", "file")
|
||||
requirePositiveInt(&errs, "bi_log.memory_limit", c.BILog.MemoryLimit)
|
||||
requireString(&errs, "bi_log.schema_path", c.BILog.SchemaPath)
|
||||
if c.BILog.Kind == "file" {
|
||||
requireString(&errs, "bi_log.file_path", c.BILog.FilePath)
|
||||
}
|
||||
if productionLike && c.BILog.Kind == "memory" {
|
||||
errs = append(errs, fmt.Errorf("bi_log.kind memory is only allowed for local/dev/test environments"))
|
||||
}
|
||||
}
|
||||
if productionLike && isEventLogCritical(c.Service.Name) && !c.EventLog.Enabled {
|
||||
errs = append(errs, fmt.Errorf("eventlog.enabled is required for production %s services", c.Service.Name))
|
||||
}
|
||||
if c.EventLog.Enabled {
|
||||
requireOneOf(&errs, "eventlog.store_kind", c.EventLog.StoreKind, "memory", "mysql")
|
||||
requireString(&errs, "eventlog.admin_prefix", c.EventLog.AdminPrefix)
|
||||
if c.EventLog.StoreKind == "memory" && (c.EventLog.Strict || productionLike) {
|
||||
errs = append(errs, fmt.Errorf("eventlog.store_kind memory is only allowed for local/dev/test eventlog paths"))
|
||||
}
|
||||
if c.EventLog.StoreKind == "mysql" {
|
||||
requireString(&errs, "eventlog.mysql_dsn", c.EventLog.MySQLDSN)
|
||||
requireSQLIdentifier(&errs, "eventlog.mysql_table", c.EventLog.MySQLTable)
|
||||
requirePositiveInt(&errs, "eventlog.mysql_max_open_conns", c.EventLog.MySQLMaxOpenConns)
|
||||
requirePositiveInt(&errs, "eventlog.mysql_max_idle_conns", c.EventLog.MySQLMaxIdleConns)
|
||||
requirePositiveInt(&errs, "eventlog.mysql_conn_max_lifetime_seconds", c.EventLog.MySQLConnMaxLifetime)
|
||||
}
|
||||
if c.EventLog.PublishEnabled {
|
||||
requireOneOf(&errs, "eventlog.publish_kind", c.EventLog.PublishKind, "bus", "http", "bus_http")
|
||||
requirePositiveInt(&errs, "eventlog.publish_interval_seconds", c.EventLog.PublishIntervalSeconds)
|
||||
requirePositiveInt(&errs, "eventlog.publish_batch_size", c.EventLog.PublishBatchSize)
|
||||
requirePositiveInt(&errs, "eventlog.publish_retry_seconds", c.EventLog.PublishRetrySeconds)
|
||||
requirePositiveInt(&errs, "eventlog.publish_max_attempts", c.EventLog.PublishMaxAttempts)
|
||||
if c.EventLog.PublishKind == "http" || c.EventLog.PublishKind == "bus_http" {
|
||||
requireString(&errs, "eventlog.publish_http_url", c.EventLog.PublishHTTPURL)
|
||||
requireString(&errs, "eventlog.publish_http_method", c.EventLog.PublishHTTPMethod)
|
||||
requirePositiveInt(&errs, "eventlog.publish_http_timeout_seconds", c.EventLog.PublishHTTPTimeout)
|
||||
requireOneOf(&errs, "eventlog.publish_http_payload", c.EventLog.PublishHTTPPayload, "event", "payload")
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.AccountCenter.Enabled {
|
||||
requireOneOf(&errs, "account_center.store_kind", c.AccountCenter.StoreKind, "memory", "mysql")
|
||||
if !strings.HasPrefix(c.AccountCenter.AdminPrefix, "/") {
|
||||
errs = append(errs, fmt.Errorf("account_center.admin_prefix %q must start with /", c.AccountCenter.AdminPrefix))
|
||||
}
|
||||
if c.AccountCenter.StoreKind == "memory" && productionLike {
|
||||
errs = append(errs, fmt.Errorf("account_center.store_kind memory is only allowed for local/dev/test account center paths"))
|
||||
}
|
||||
if c.AccountCenter.StoreKind == "mysql" {
|
||||
requireString(&errs, "account_center.mysql_dsn", c.AccountCenter.MySQLDSN)
|
||||
requireSQLIdentifier(&errs, "account_center.mysql_table", c.AccountCenter.MySQLTable)
|
||||
}
|
||||
}
|
||||
if c.StorageObject.Enabled {
|
||||
requireOneOf(&errs, "storage_object.store_kind", c.StorageObject.StoreKind, "memory", "mysql")
|
||||
if !strings.HasPrefix(c.StorageObject.AdminPrefix, "/") {
|
||||
errs = append(errs, fmt.Errorf("storage_object.admin_prefix %q must start with /", c.StorageObject.AdminPrefix))
|
||||
}
|
||||
if c.StorageObject.StoreKind == "memory" && productionLike {
|
||||
errs = append(errs, fmt.Errorf("storage_object.store_kind memory is only allowed for local/dev/test storage object paths"))
|
||||
}
|
||||
if c.StorageObject.StoreKind == "mysql" {
|
||||
requireString(&errs, "storage_object.mysql_dsn", c.StorageObject.MySQLDSN)
|
||||
requireSQLIdentifier(&errs, "storage_object.mysql_table", c.StorageObject.MySQLTable)
|
||||
}
|
||||
}
|
||||
if isBattleAgentService(c.Service.Name) {
|
||||
requireOneOf(&errs, "battle_agent.settlement_outbox_kind", c.BattleAgent.SettlementOutboxKind, "memory", "eventlog")
|
||||
requireSafeName(&errs, "battle_agent.settlement_outbox_stream", c.BattleAgent.SettlementOutboxStream)
|
||||
requireSafeName(&errs, "battle_agent.settlement_outbox_type", c.BattleAgent.SettlementOutboxType)
|
||||
if c.BattleAgent.SettlementOutboxKind == "eventlog" && !c.EventLog.Enabled {
|
||||
errs = append(errs, fmt.Errorf("eventlog.enabled is required when battle_agent.settlement_outbox_kind is eventlog"))
|
||||
}
|
||||
if productionLike && c.BattleAgent.SettlementOutboxKind != "eventlog" {
|
||||
errs = append(errs, fmt.Errorf("battle_agent.settlement_outbox_kind eventlog is required for production battleagent services"))
|
||||
}
|
||||
}
|
||||
requirePositiveInt(&errs, "reload.poll_interval_seconds", c.Reload.PollIntervalSeconds)
|
||||
requirePositiveInt(&errs, "reload.apply_timeout_seconds", c.Reload.ApplyTimeoutSeconds)
|
||||
if c.HotUpdate.Enabled {
|
||||
requireSafeName(&errs, "hot_update.target", c.HotUpdate.Target)
|
||||
requireOneOf(&errs, "hot_update.control_kind", c.HotUpdate.ControlKind, "memory", "eventlog")
|
||||
requireOneOf(&errs, "hot_update.source_kind", c.HotUpdate.SourceKind, "local")
|
||||
requireOneOf(&errs, "hot_update.applier_kind", c.HotUpdate.ApplierKind, "datatable", "native_patch")
|
||||
requireString(&errs, "hot_update.workspace", c.HotUpdate.Workspace)
|
||||
requirePositiveInt(&errs, "hot_update.apply_timeout_seconds", c.HotUpdate.ApplyTimeoutSeconds)
|
||||
requireNonNegInt(&errs, "hot_update.native_patch_max_symbols", c.HotUpdate.NativePatchMaxSymbols)
|
||||
requireNonNegInt(&errs, "hot_update.native_patch_max_live_seconds", c.HotUpdate.NativePatchMaxLiveSeconds)
|
||||
requireNonNegInt(&errs, "hot_update.native_patch_min_reason_length", c.HotUpdate.NativePatchMinReasonLength)
|
||||
if c.HotUpdate.RequireSignature {
|
||||
requireString(&errs, "hot_update.signing_key_env", c.HotUpdate.SigningKeyEnv)
|
||||
}
|
||||
validatePatchPolicy(&errs, c.HotUpdate, productionLike)
|
||||
if c.HotUpdate.ControlKind == "eventlog" && !c.EventLog.Enabled {
|
||||
errs = append(errs, fmt.Errorf("eventlog.enabled is required when hot_update.control_kind is eventlog"))
|
||||
}
|
||||
if productionLike && c.HotUpdate.ControlKind != "eventlog" {
|
||||
errs = append(errs, fmt.Errorf("hot_update.control_kind eventlog is required for production-like hot update"))
|
||||
}
|
||||
}
|
||||
if c.StateSync.Enabled {
|
||||
requireOneOf(&errs, "state_sync.kind", c.StateSync.Kind, "memory")
|
||||
requireSafeName(&errs, "state_sync.namespace", c.StateSync.Namespace)
|
||||
requirePositiveInt(&errs, "state_sync.ttl_seconds", c.StateSync.TTLSeconds)
|
||||
requirePositiveInt(&errs, "state_sync.cleanup_interval_seconds", c.StateSync.CleanupIntervalSeconds)
|
||||
requirePositiveInt(&errs, "state_sync.max_payload_bytes", c.StateSync.MaxPayloadBytes)
|
||||
}
|
||||
if c.ServerGroup.Enabled {
|
||||
requireOneOf(&errs, "server_group.store_kind", c.ServerGroup.StoreKind, "file")
|
||||
requireString(&errs, "server_group.plan_file", c.ServerGroup.PlanFile)
|
||||
requireString(&errs, "server_group.merge_archive_dir", c.ServerGroup.MergeArchiveDir)
|
||||
if !strings.HasPrefix(c.ServerGroup.AdminPrefix, "/") {
|
||||
errs = append(errs, fmt.Errorf("server_group.admin_prefix %q must start with /", c.ServerGroup.AdminPrefix))
|
||||
}
|
||||
requirePositiveInt(&errs, "server_group.in_zone_delay_seconds", c.ServerGroup.InZoneDelaySeconds)
|
||||
requirePositiveInt(&errs, "server_group.notice_lead_seconds", c.ServerGroup.NoticeLeadSeconds)
|
||||
}
|
||||
if c.Topology.Enabled {
|
||||
requireNonEmptySlice(&errs, "topology.services", c.Topology.Services)
|
||||
requireSafeName(&errs, "topology.gateway_service", c.Topology.GatewayService)
|
||||
requireSafeName(&errs, "topology.gateway_backend_service", c.Topology.GatewayBackendService)
|
||||
requirePositiveInt(&errs, "topology.refresh_interval_seconds", c.Topology.RefreshIntervalSeconds)
|
||||
for idx, service := range c.Topology.Services {
|
||||
requireSafeName(&errs, fmt.Sprintf("topology.services[%d]", idx), service)
|
||||
}
|
||||
}
|
||||
requirePositiveInt(&errs, "discovery.cache_ttl_seconds", c.Discovery.CacheTTLSeconds)
|
||||
requirePositiveInt(&errs, "discovery.failure_ttl_seconds", c.Discovery.FailureTTLSeconds)
|
||||
requirePositiveInt(&errs, "discovery.failure_threshold", c.Discovery.FailureThreshold)
|
||||
requireOneOf(&errs, "discovery.strategy", c.Discovery.Strategy, "hash", "random", "round_robin", "weighted_round_robin")
|
||||
requirePositiveInt(&errs, "rpc.call_timeout_seconds", c.RPC.CallTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "rpc.max_pending", c.RPC.MaxPending)
|
||||
requirePositiveInt(&errs, "rpc.max_payload_bytes", c.RPC.MaxPayloadBytes)
|
||||
|
||||
requireOneOf(&errs, "idempotency.kind", c.Idempotency.Kind, "memory", "redis", "mysql", "mysql_redis")
|
||||
if productionLike && isIdemCriticalSvc(c.Service.Name) && c.Idempotency.Kind != "mysql_redis" {
|
||||
errs = append(errs, fmt.Errorf("idempotency.kind mysql_redis is required for production %s services", c.Service.Name))
|
||||
}
|
||||
requirePositiveInt(&errs, "idempotency.ttl_seconds", c.Idempotency.TTLSeconds)
|
||||
requireSafeName(&errs, "idempotency.key_prefix", c.Idempotency.KeyPrefix)
|
||||
if c.Idempotency.Kind == "redis" || c.Idempotency.Kind == "mysql_redis" {
|
||||
requireString(&errs, "idempotency.redis_address", c.Idempotency.RedisAddress)
|
||||
requireNonNegInt(&errs, "idempotency.redis_db", c.Idempotency.RedisDB)
|
||||
requirePositiveInt(&errs, "idempotency.redis_pool_size", c.Idempotency.RedisPoolSize)
|
||||
requirePositiveInt(&errs, "idempotency.redis_timeout_seconds", c.Idempotency.RedisTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "idempotency.redis_connect_timeout_seconds", c.Idempotency.RedisConnectTimeoutSecs)
|
||||
requirePositiveInt(&errs, "idempotency.redis_read_timeout_seconds", c.Idempotency.RedisReadTimeoutSecs)
|
||||
requirePositiveInt(&errs, "idempotency.redis_write_timeout_seconds", c.Idempotency.RedisWriteTimeoutSecs)
|
||||
}
|
||||
if c.Idempotency.Kind == "mysql" || c.Idempotency.Kind == "mysql_redis" {
|
||||
requireString(&errs, "idempotency.mysql_dsn", c.Idempotency.MySQLDSN)
|
||||
requirePositiveInt(&errs, "idempotency.mysql_max_open_conns", c.Idempotency.MySQLMaxOpenConns)
|
||||
requirePositiveInt(&errs, "idempotency.mysql_max_idle_conns", c.Idempotency.MySQLMaxIdleConns)
|
||||
requirePositiveInt(&errs, "idempotency.mysql_conn_max_lifetime_seconds", c.Idempotency.MySQLConnMaxLifetimeSec)
|
||||
}
|
||||
|
||||
requireOneOf(&errs, "registry.kind", c.Registry.Kind, "memory", "redis", "etcd")
|
||||
requireSafeName(&errs, "registry.namespace", c.Registry.Namespace)
|
||||
requirePositiveInt64(&errs, "registry.lease_ttl", c.Registry.LeaseTTL)
|
||||
if c.Registry.Kind == "redis" || c.Registry.Kind == "etcd" {
|
||||
requireNonEmptySlice(&errs, "registry.endpoints", c.Registry.Endpoints)
|
||||
}
|
||||
if c.Registry.Kind == "redis" {
|
||||
requireNonNegInt(&errs, "registry.redis_db", c.Registry.RedisDB)
|
||||
}
|
||||
|
||||
requireOneOf(&errs, "bus.kind", c.Bus.Kind, "memory", "redis", "nats")
|
||||
requireSafeName(&errs, "bus.namespace", c.Bus.Namespace)
|
||||
if c.Bus.Kind == "redis" || c.Bus.Kind == "nats" {
|
||||
requireNonEmptySlice(&errs, "bus.endpoints", c.Bus.Endpoints)
|
||||
}
|
||||
if c.Bus.Kind == "redis" {
|
||||
requireNonNegInt(&errs, "bus.redis_db", c.Bus.RedisDB)
|
||||
if c.Bus.RedisUsername != "" && c.Bus.RedisPassword == "" {
|
||||
errs = append(errs, fmt.Errorf("bus.redis_password is required when bus.redis_username is set"))
|
||||
}
|
||||
}
|
||||
if c.Bus.Kind == "nats" {
|
||||
requireOneOf(&errs, "bus.nats_wire_encoding", c.Bus.NATSWireEncoding, "binary_v1", "json")
|
||||
if c.Bus.NATSPassword != "" && c.Bus.NATSUsername == "" {
|
||||
errs = append(errs, fmt.Errorf("bus.nats_username is required when bus.nats_password is set"))
|
||||
}
|
||||
if (c.Bus.NATSCertFile == "") != (c.Bus.NATSKeyFile == "") {
|
||||
errs = append(errs, fmt.Errorf("bus.nats_cert_file and bus.nats_key_file must be configured together"))
|
||||
}
|
||||
}
|
||||
|
||||
requireOneOf(&errs, "cache.kind", c.Cache.Kind, "memory", "redis")
|
||||
requireSafeName(&errs, "cache.key_prefix", c.Cache.KeyPrefix)
|
||||
requirePositiveInt(&errs, "cache.default_ttl_seconds", c.Cache.DefaultTTLSeconds)
|
||||
requirePositiveInt(&errs, "cache.max_entries", c.Cache.MaxEntries)
|
||||
if c.Cache.Kind == "redis" {
|
||||
requireString(&errs, "cache.redis_address", c.Cache.RedisAddress)
|
||||
requireNonNegInt(&errs, "cache.redis_db", c.Cache.RedisDB)
|
||||
requirePositiveInt(&errs, "cache.redis_pool_size", c.Cache.RedisPoolSize)
|
||||
requirePositiveInt(&errs, "cache.redis_timeout_seconds", c.Cache.RedisTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "cache.redis_connect_timeout_seconds", c.Cache.RedisConnectTimeout)
|
||||
requirePositiveInt(&errs, "cache.redis_read_timeout_seconds", c.Cache.RedisReadTimeout)
|
||||
requirePositiveInt(&errs, "cache.redis_write_timeout_seconds", c.Cache.RedisWriteTimeout)
|
||||
}
|
||||
|
||||
requireOneOf(&errs, "lock.kind", c.Lock.Kind, "memory", "redis")
|
||||
requireSafeName(&errs, "lock.key_prefix", c.Lock.KeyPrefix)
|
||||
requirePositiveInt(&errs, "lock.ttl_seconds", c.Lock.TTLSeconds)
|
||||
if c.Lock.Kind == "redis" {
|
||||
requireString(&errs, "lock.redis_address", c.Lock.RedisAddress)
|
||||
requireNonNegInt(&errs, "lock.redis_db", c.Lock.RedisDB)
|
||||
requirePositiveInt(&errs, "lock.redis_pool_size", c.Lock.RedisPoolSize)
|
||||
requirePositiveInt(&errs, "lock.redis_timeout_seconds", c.Lock.RedisTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "lock.redis_connect_timeout_seconds", c.Lock.RedisConnectTimeout)
|
||||
requirePositiveInt(&errs, "lock.redis_read_timeout_seconds", c.Lock.RedisReadTimeout)
|
||||
requirePositiveInt(&errs, "lock.redis_write_timeout_seconds", c.Lock.RedisWriteTimeout)
|
||||
}
|
||||
|
||||
requireOneOf(&errs, "presence.kind", c.Presence.Kind, "memory", "redis")
|
||||
requireSafeName(&errs, "presence.key_prefix", c.Presence.KeyPrefix)
|
||||
requirePositiveInt(&errs, "presence.ttl_seconds", c.Presence.TTLSeconds)
|
||||
if c.Presence.Kind == "redis" {
|
||||
requireString(&errs, "presence.redis_address", c.Presence.RedisAddress)
|
||||
requireNonNegInt(&errs, "presence.redis_db", c.Presence.RedisDB)
|
||||
requirePositiveInt(&errs, "presence.redis_pool_size", c.Presence.RedisPoolSize)
|
||||
requirePositiveInt(&errs, "presence.redis_timeout_seconds", c.Presence.RedisTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "presence.redis_connect_timeout_seconds", c.Presence.RedisConnectTimeout)
|
||||
requirePositiveInt(&errs, "presence.redis_read_timeout_seconds", c.Presence.RedisReadTimeout)
|
||||
requirePositiveInt(&errs, "presence.redis_write_timeout_seconds", c.Presence.RedisWriteTimeout)
|
||||
}
|
||||
|
||||
requirePositiveInt(&errs, "worker.size", c.Worker.Size)
|
||||
requirePositiveInt(&errs, "worker.queue", c.Worker.Queue)
|
||||
|
||||
requireOneOf(&errs, "profile_store.store_kind", c.ProfileStore.StoreKind, "memory", "file", "json", "redis", "pika", "profiledb", "profile_db", "mysql", "mysql_redis")
|
||||
if c.ProfileStore.StoreKind == "file" || c.ProfileStore.StoreKind == "json" {
|
||||
requireString(&errs, "profile_store.store_directory", c.ProfileStore.StoreDirectory)
|
||||
}
|
||||
if c.ProfileStore.StoreKind == "redis" || c.ProfileStore.StoreKind == "pika" || c.ProfileStore.StoreKind == "profiledb" || c.ProfileStore.StoreKind == "profile_db" {
|
||||
requireString(&errs, "profile_store.store_address", c.ProfileStore.StoreAddress)
|
||||
requireString(&errs, "profile_store.store_key_prefix", c.ProfileStore.StoreKeyPrefix)
|
||||
}
|
||||
if c.ProfileStore.StoreKind == "mysql" || c.ProfileStore.StoreKind == "mysql_redis" {
|
||||
if !c.ProfileStore.MySQLShardingEnabled && len(c.ProfileStore.MySQLShards) == 0 {
|
||||
requireString(&errs, "profile_store.mysql_dsn", c.ProfileStore.MySQLDSN)
|
||||
}
|
||||
requirePositiveInt(&errs, "profile_store.mysql_max_open_conns", c.ProfileStore.MySQLMaxOpenConns)
|
||||
requirePositiveInt(&errs, "profile_store.mysql_max_idle_conns", c.ProfileStore.MySQLMaxIdleConns)
|
||||
requirePositiveInt(&errs, "profile_store.mysql_conn_max_lifetime_seconds", c.ProfileStore.MySQLConnMaxLifetime)
|
||||
validateMySQLShards(&errs, c.ProfileStore)
|
||||
} else if c.ProfileStore.MySQLShardingEnabled || len(c.ProfileStore.MySQLShards) > 0 {
|
||||
errs = append(errs, fmt.Errorf("profile_store.mysql_shards require profile_store.store_kind mysql or mysql_redis"))
|
||||
}
|
||||
requireOneOf(&errs, "profile_store.summary_store_kind", c.ProfileStore.SummaryStoreKind, "memory", "redis", "pika")
|
||||
if c.ProfileStore.SummaryStoreKind == "redis" || c.ProfileStore.SummaryStoreKind == "pika" {
|
||||
requireString(&errs, "profile_store.summary_store_address", c.ProfileStore.SummaryStoreAddress)
|
||||
requireString(&errs, "profile_store.summary_store_key_prefix", c.ProfileStore.SummaryStoreKeyPrefix)
|
||||
requirePositiveInt(&errs, "profile_store.summary_store_pool_size", c.ProfileStore.SummaryStorePoolSize)
|
||||
requirePositiveInt(&errs, "profile_store.summary_store_timeout_seconds", c.ProfileStore.SummaryStoreTimeout)
|
||||
requirePositiveInt(&errs, "profile_store.summary_store_connect_timeout_seconds", c.ProfileStore.SummaryStoreConnectTimeout)
|
||||
requirePositiveInt(&errs, "profile_store.summary_store_read_timeout_seconds", c.ProfileStore.SummaryStoreReadTimeout)
|
||||
requirePositiveInt(&errs, "profile_store.summary_store_write_timeout_seconds", c.ProfileStore.SummaryStoreWriteTimeout)
|
||||
requireNonNegInt(&errs, "profile_store.summary_store_db", c.ProfileStore.SummaryStoreDB)
|
||||
requireNonNegInt(&errs, "profile_store.summary_store_ttl_seconds", c.ProfileStore.SummaryStoreTTL)
|
||||
}
|
||||
requireNonNegInt(&errs, "profile_store.store_db", c.ProfileStore.StoreDB)
|
||||
requirePositiveInt(&errs, "profile_store.store_pool_size", c.ProfileStore.StorePoolSize)
|
||||
requirePositiveInt(&errs, "profile_store.store_timeout_seconds", c.ProfileStore.StoreTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "profile_store.store_connect_timeout_seconds", c.ProfileStore.StoreConnectTimeout)
|
||||
requirePositiveInt(&errs, "profile_store.store_read_timeout_seconds", c.ProfileStore.StoreReadTimeout)
|
||||
requirePositiveInt(&errs, "profile_store.store_write_timeout_seconds", c.ProfileStore.StoreWriteTimeout)
|
||||
requireNonNegInt(&errs, "profile_store.store_ttl_seconds", c.ProfileStore.StoreTTLSeconds)
|
||||
requireOneOf(&errs, "profile_store.save_mode", c.ProfileStore.SaveMode, "sync", "async", "writebehind", "write_behind")
|
||||
requirePositiveInt(&errs, "profile_store.async_flush_interval_seconds", c.ProfileStore.AsyncFlushIntervalSeconds)
|
||||
requirePositiveInt(&errs, "profile_store.async_max_pending", c.ProfileStore.AsyncMaxPending)
|
||||
requirePositiveInt(&errs, "profile_store.async_retry_backoff_seconds", c.ProfileStore.AsyncRetryBackoffSeconds)
|
||||
requireNonNegInt(&errs, "profile_store.async_max_retries", c.ProfileStore.AsyncMaxRetries)
|
||||
requireNonNegInt(&errs, "profile_store.async_auto_expire_seconds", c.ProfileStore.AsyncAutoExpireSeconds)
|
||||
requirePositiveInt(&errs, "profile_store.async_dead_letter_limit", c.ProfileStore.AsyncDeadLetterLimit)
|
||||
if isAsyncSaveMode(c.ProfileStore.SaveMode) {
|
||||
requireString(&errs, "profile_store.async_dead_letter_directory", c.ProfileStore.AsyncDeadLetterDirectory)
|
||||
if productionLike && c.ProfileStore.AsyncMaxRetries <= 0 {
|
||||
errs = append(errs, fmt.Errorf("profile_store.async_max_retries must be positive for production-like async profile stores; 0 means retry forever"))
|
||||
}
|
||||
}
|
||||
|
||||
requireListenAddress(&errs, "gateway.auth_listen", c.Gateway.AuthListen)
|
||||
requireOneOf(&errs, "gateway.auth_mode", c.Gateway.AuthMode, "mock", "external", "disabled")
|
||||
if isGatewayService(c.Service.Name) && productionLike && c.Gateway.AuthMode != "external" {
|
||||
errs = append(errs, fmt.Errorf("gateway.auth_mode external is required for production gateway services"))
|
||||
}
|
||||
if c.Gateway.RegistryGateSelection {
|
||||
requireString(&errs, "gateway.gate_service", c.Gateway.GateService)
|
||||
requireString(&errs, "gateway.gate_route_feature", c.Gateway.GateRouteFeature)
|
||||
}
|
||||
if c.Gateway.RemoteGateSelection && !c.Gateway.RegistryGateSelection {
|
||||
errs = append(errs, fmt.Errorf("gateway.remote_gate_selection requires gateway.registry_gate_selection"))
|
||||
}
|
||||
if c.Gateway.RemoteGateSelection && !c.Gateway.SharedLoginTokens {
|
||||
errs = append(errs, fmt.Errorf("gateway.remote_gate_selection requires gateway.shared_login_tokens"))
|
||||
}
|
||||
if isGatewayService(c.Service.Name) && productionLike && c.Gateway.RemoteGateSelection && c.Cache.Kind != "redis" {
|
||||
errs = append(errs, fmt.Errorf("gateway.remote_gate_selection requires cache.kind=redis for production gateway services"))
|
||||
}
|
||||
requireListenAddress(&errs, "gateway.tcp_listen", c.Gateway.TCPListen)
|
||||
if c.Gateway.TCPProxyProtocolEnabled {
|
||||
requirePositiveInt(&errs, "gateway.tcp_proxy_header_timeout_seconds", c.Gateway.TCPProxyHeaderSec)
|
||||
requireNonEmptySlice(&errs, "gateway.tcp_proxy_trusted_cidrs", c.Gateway.TCPProxyTrustedCIDRs)
|
||||
for idx, value := range c.Gateway.TCPProxyTrustedCIDRs {
|
||||
if err := validateIPOrCIDR(value); err != nil {
|
||||
errs = append(errs, fmt.Errorf("gateway.tcp_proxy_trusted_cidrs[%d]: %w", idx, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.Gateway.KCPEnabled {
|
||||
requireListenAddress(&errs, "gateway.kcp_listen", c.Gateway.KCPListen)
|
||||
requireNonNegInt(&errs, "gateway.kcp_nodelay", c.Gateway.KCPNoDelay)
|
||||
requirePositiveInt(&errs, "gateway.kcp_interval", c.Gateway.KCPInterval)
|
||||
requireNonNegInt(&errs, "gateway.kcp_resend", c.Gateway.KCPResend)
|
||||
requireNonNegInt(&errs, "gateway.kcp_no_congestion", c.Gateway.KCPNoCongestion)
|
||||
requirePositiveInt(&errs, "gateway.kcp_window_size", c.Gateway.KCPWindowSize)
|
||||
requireNonNegInt(&errs, "gateway.kcp_data_shards", c.Gateway.KCPDataShards)
|
||||
requireNonNegInt(&errs, "gateway.kcp_parity_shards", c.Gateway.KCPParityShards)
|
||||
}
|
||||
if c.Gateway.QUICEnabled {
|
||||
requireListenAddress(&errs, "gateway.quic_listen", c.Gateway.QUICListen)
|
||||
requirePositiveInt(&errs, "gateway.quic_handshake_timeout_seconds", c.Gateway.QUICHandshakeSec)
|
||||
requirePositiveInt(&errs, "gateway.quic_max_idle_timeout_seconds", c.Gateway.QUICMaxIdleTimeoutSeconds)
|
||||
requireNonNegInt(&errs, "gateway.quic_keep_alive_seconds", c.Gateway.QUICKeepAliveSeconds)
|
||||
requirePositiveInt64(&errs, "gateway.quic_max_incoming_streams", c.Gateway.QUICMaxIncomingStreams)
|
||||
if (c.Gateway.QUICCertFile == "") != (c.Gateway.QUICKeyFile == "") {
|
||||
errs = append(errs, fmt.Errorf("gateway.quic_cert_file and gateway.quic_key_file must be set together"))
|
||||
}
|
||||
if productionLike && c.Gateway.QUICCertFile == "" && c.Gateway.QUICKeyFile == "" {
|
||||
errs = append(errs, fmt.Errorf("gateway.quic_cert_file and gateway.quic_key_file are required for production-like QUIC"))
|
||||
}
|
||||
}
|
||||
if c.Gateway.WebSocketEnabled {
|
||||
requireListenAddress(&errs, "gateway.websocket_listen", c.Gateway.WebSocketListen)
|
||||
if !strings.HasPrefix(c.Gateway.WebSocketPath, "/") {
|
||||
errs = append(errs, fmt.Errorf("gateway.websocket_path %q must start with /", c.Gateway.WebSocketPath))
|
||||
}
|
||||
requirePositiveInt(&errs, "gateway.websocket_max_payload_bytes", c.Gateway.WebSocketMaxPayloadBytes)
|
||||
requirePositiveInt(&errs, "gateway.websocket_heartbeat_seconds", c.Gateway.WebSocketHeartbeatSeconds)
|
||||
}
|
||||
requireListenAddress(&errs, "gateway.chat_listen", c.Gateway.ChatListen)
|
||||
requireSafeName(&errs, "gateway.backend_service", c.Gateway.BackendService)
|
||||
requirePositiveInt(&errs, "gateway.backend_poll_interval_seconds", c.Gateway.BackendPollIntervalSeconds)
|
||||
requirePositiveInt(&errs, "gateway.max_connections", c.Gateway.MaxConnections)
|
||||
requirePositiveInt(&errs, "gateway.connection_rate_limit_window_seconds", c.Gateway.ConnRateWindowSec)
|
||||
requirePositiveInt(&errs, "gateway.connection_rate_limit_max", c.Gateway.ConnectionRateLimitMax)
|
||||
requirePositiveInt(&errs, "gateway.connection_rate_limit_max_entries", c.Gateway.ConnRateMaxEntries)
|
||||
if c.Gateway.ConnRateIPv4Prefix < 1 || c.Gateway.ConnRateIPv4Prefix > 32 {
|
||||
errs = append(errs, fmt.Errorf("gateway.connection_rate_limit_ipv4_prefix must be between 1 and 32"))
|
||||
}
|
||||
if c.Gateway.ConnRateIPv6Prefix < 1 || c.Gateway.ConnRateIPv6Prefix > 128 {
|
||||
errs = append(errs, fmt.Errorf("gateway.connection_rate_limit_ipv6_prefix must be between 1 and 128"))
|
||||
}
|
||||
requirePositiveInt(&errs, "gateway.session_idle_timeout_seconds", c.Gateway.SessionIdleTimeoutSeconds)
|
||||
requirePositiveInt(&errs, "gateway.session_sweep_interval_seconds", c.Gateway.SessionSweepSec)
|
||||
requirePositiveInt(&errs, "gateway.session_hook_timeout_seconds", c.Gateway.SessionHookTimeoutSeconds)
|
||||
requireString(&errs, "gateway.account_id", c.Gateway.AccountID)
|
||||
requirePositiveInt32(&errs, "gateway.account_num_id", c.Gateway.AccountNumID)
|
||||
if c.Gateway.AuthMode == "mock" {
|
||||
requireString(&errs, "gateway.auth_token", c.Gateway.AuthToken)
|
||||
requireString(&errs, "gateway.login_token", c.Gateway.LoginToken)
|
||||
}
|
||||
requireString(&errs, "gateway.server_version", c.Gateway.ServerVersion)
|
||||
requirePositiveInt32(&errs, "gateway.shard_id", c.Gateway.ShardID)
|
||||
requireString(&errs, "gateway.shard_name", c.Gateway.ShardName)
|
||||
requireString(&errs, "gateway.shard_display", c.Gateway.ShardDisplay)
|
||||
requirePositiveInt32(&errs, "gateway.shard_state", c.Gateway.ShardState)
|
||||
requirePositiveInt32(&errs, "gateway.recommend", c.Gateway.Recommend)
|
||||
requireString(&errs, "gateway.recommend_lang", c.Gateway.RecommendLang)
|
||||
requirePositiveInt32(&errs, "gateway.virtual_gid", c.Gateway.VirtualGID)
|
||||
requireNonEmptySlice(&errs, "gateway.product_ids", c.Gateway.ProductIDs)
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func validateIPOrCIDR(value string) error {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return fmt.Errorf("empty value")
|
||||
}
|
||||
if net.ParseIP(value) != nil {
|
||||
return nil
|
||||
}
|
||||
if _, _, err := net.ParseCIDR(value); err != nil {
|
||||
return fmt.Errorf("invalid IP or CIDR %q", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func requireString(errs *[]error, field, value string) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
*errs = append(*errs, fmt.Errorf("%s is required", field))
|
||||
}
|
||||
}
|
||||
|
||||
func isAsyncSaveMode(mode string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||
case "async", "writebehind", "write_behind":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func requireSafeName(errs *[]error, field, value string) {
|
||||
requireString(errs, field, value)
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
for _, r := range value {
|
||||
if isSafeNameRune(r) {
|
||||
continue
|
||||
}
|
||||
*errs = append(*errs, fmt.Errorf("%s %q can only contain letters, digits, dot, dash, or underscore", field, value))
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(value, ".") || strings.HasSuffix(value, ".") || strings.Contains(value, "..") {
|
||||
*errs = append(*errs, fmt.Errorf("%s %q cannot start with dot, end with dot, or contain consecutive dots", field, value))
|
||||
}
|
||||
}
|
||||
|
||||
func requireSQLIdentifier(errs *[]error, field, value string) {
|
||||
requireString(errs, field, value)
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
for idx, r := range value {
|
||||
if idx == 0 {
|
||||
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
|
||||
continue
|
||||
}
|
||||
*errs = append(*errs, fmt.Errorf("%s %q must start with a letter or underscore", field, value))
|
||||
return
|
||||
}
|
||||
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||
continue
|
||||
}
|
||||
*errs = append(*errs, fmt.Errorf("%s %q can only contain letters, digits, or underscore", field, value))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func isSafeNameRune(r rune) bool {
|
||||
return r == '.' || r == '-' || r == '_' ||
|
||||
(r >= 'a' && r <= 'z') ||
|
||||
(r >= 'A' && r <= 'Z') ||
|
||||
(r >= '0' && r <= '9')
|
||||
}
|
||||
|
||||
func requireNonEmptySlice(errs *[]error, field string, values []string) {
|
||||
if len(values) == 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s requires at least one value", field))
|
||||
return
|
||||
}
|
||||
for idx, value := range values {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
*errs = append(*errs, fmt.Errorf("%s[%d] is required", field, idx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireOneOf(errs *[]error, field, value string, allowed ...string) {
|
||||
if !slices.Contains(allowed, value) {
|
||||
*errs = append(*errs, fmt.Errorf("%s %q must be one of %s", field, value, strings.Join(allowed, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
func requirePositiveInt(errs *[]error, field string, value int) {
|
||||
if value <= 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s must be positive", field))
|
||||
}
|
||||
}
|
||||
|
||||
func requirePositiveInt32(errs *[]error, field string, value int32) {
|
||||
if value <= 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s must be positive", field))
|
||||
}
|
||||
}
|
||||
|
||||
func requirePositiveInt64(errs *[]error, field string, value int64) {
|
||||
if value <= 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s must be positive", field))
|
||||
}
|
||||
}
|
||||
|
||||
func requireNonNegInt(errs *[]error, field string, value int) {
|
||||
if value < 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s must be non-negative", field))
|
||||
}
|
||||
}
|
||||
|
||||
func requireNonNegInt64(errs *[]error, field string, value int64) {
|
||||
if value < 0 {
|
||||
*errs = append(*errs, fmt.Errorf("%s must be non-negative", field))
|
||||
}
|
||||
}
|
||||
|
||||
func requireListenAddress(errs *[]error, field, value string) {
|
||||
requireString(errs, field, value)
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
_, port, err := net.SplitHostPort(value)
|
||||
if err != nil {
|
||||
*errs = append(*errs, fmt.Errorf("%s %q must be host:port or :port: %w", field, value, err))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(port) == "" {
|
||||
*errs = append(*errs, fmt.Errorf("%s %q must include a port", field, value))
|
||||
return
|
||||
}
|
||||
if _, err := net.LookupPort("tcp", port); err != nil {
|
||||
*errs = append(*errs, fmt.Errorf("%s %q has invalid port %q: %w", field, value, port, err))
|
||||
}
|
||||
}
|
||||
|
||||
func validRateRules(raw string) error {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
for _, part := range strings.Split(raw, ";") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(part, ",")
|
||||
if len(fields) != 3 {
|
||||
return fmt.Errorf("rate_limit.rules %q must use path,window_seconds,max_requests", part)
|
||||
}
|
||||
if strings.TrimSpace(fields[0]) == "" {
|
||||
return fmt.Errorf("rate_limit.rules %q has empty path", part)
|
||||
}
|
||||
window, err := strconv.Atoi(strings.TrimSpace(fields[1]))
|
||||
if err != nil || window <= 0 {
|
||||
return fmt.Errorf("rate_limit.rules %q has invalid window_seconds", part)
|
||||
}
|
||||
max, err := strconv.Atoi(strings.TrimSpace(fields[2]))
|
||||
if err != nil || max <= 0 {
|
||||
return fmt.Errorf("rate_limit.rules %q has invalid max_requests", part)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func validatePatchPolicy(errs *[]error, section HotUpdateSection, productionLike bool) {
|
||||
if section.ApplierKind != "native_patch" {
|
||||
return
|
||||
}
|
||||
for idx, target := range section.NativePatchAllowedTargets {
|
||||
requireSafeName(errs, fmt.Sprintf("hot_update.native_patch_allowed_targets[%d]", idx), target)
|
||||
}
|
||||
for idx, symbol := range section.PatchOldSymbols {
|
||||
field := fmt.Sprintf("hot_update.native_patch_allowed_old_symbols[%d]", idx)
|
||||
requireString(errs, field, symbol)
|
||||
if strings.TrimSpace(symbol) != symbol || strings.ContainsAny(symbol, "\r\n") {
|
||||
*errs = append(*errs, fmt.Errorf("%s cannot contain surrounding whitespace or line breaks", field))
|
||||
}
|
||||
}
|
||||
if !productionLike {
|
||||
if section.NativePatchMaxLiveSeconds <= 0 {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_max_live_seconds is required for native patch"))
|
||||
}
|
||||
return
|
||||
}
|
||||
if !section.RequireSignature {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.require_signature is required for production-like native patch"))
|
||||
}
|
||||
if strings.TrimSpace(section.SigningKeyEnv) == "" {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.signing_key_env is required for production-like native patch"))
|
||||
}
|
||||
if !section.NativePatchRequireBuildID {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_require_build_id is required for production-like native patch"))
|
||||
}
|
||||
if !section.PatchRequireActor {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_require_requested_by is required for production-like native patch"))
|
||||
}
|
||||
if !section.NativePatchRequireReason {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_require_reason is required for production-like native patch"))
|
||||
}
|
||||
if section.NativePatchMinReasonLength <= 0 {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_min_reason_length is required for production-like native patch"))
|
||||
}
|
||||
if len(section.PatchOldSymbols) == 0 {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_allowed_old_symbols requires at least one value for production-like native patch"))
|
||||
}
|
||||
if section.NativePatchMaxLiveSeconds <= 0 {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_max_live_seconds is required for production-like native patch"))
|
||||
}
|
||||
if section.NativePatchMaxLiveSeconds > 3600 {
|
||||
*errs = append(*errs, fmt.Errorf("hot_update.native_patch_max_live_seconds must be <= 3600 for production-like native patch"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user