Skip to content

Commit fa1e0ea

Browse files
corylanouclaude
andauthored
feat: implement comprehensive global replica defaults (#746)
Co-authored-by: Claude <[email protected]>
1 parent be474af commit fa1e0ea

File tree

4 files changed

+402
-33
lines changed

4 files changed

+402
-33
lines changed

cmd/litestream/main.go

Lines changed: 135 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ The commands are:
190190

191191
// Config represents a configuration file for the litestream daemon.
192192
type Config struct {
193+
// Global replica settings that serve as defaults for all replicas
194+
ReplicaSettings `yaml:",inline"`
195+
193196
// Bind address for serving metrics.
194197
Addr string `yaml:"addr"`
195198

@@ -211,10 +214,6 @@ type Config struct {
211214
// Litestream will shutdown when subcommand exits.
212215
Exec string `yaml:"exec"`
213216

214-
// Global S3 settings
215-
AccessKeyID string `yaml:"access-key-id"`
216-
SecretAccessKey string `yaml:"secret-access-key"`
217-
218217
// Logging
219218
Logging LoggingConfig `yaml:"logging"`
220219

@@ -239,16 +238,15 @@ type LoggingConfig struct {
239238
Stderr bool `yaml:"stderr"`
240239
}
241240

242-
// propagateGlobalSettings copies global S3 settings to replica configs.
241+
// propagateGlobalSettings copies global replica settings to individual replica configs.
243242
func (c *Config) propagateGlobalSettings() {
244243
for _, dbc := range c.DBs {
244+
// Handle both old-style 'replicas' and new-style 'replica'
245+
if dbc.Replica != nil {
246+
dbc.Replica.SetDefaults(&c.ReplicaSettings)
247+
}
245248
for _, rc := range dbc.Replicas {
246-
if rc.AccessKeyID == "" {
247-
rc.AccessKeyID = c.AccessKeyID
248-
}
249-
if rc.SecretAccessKey == "" {
250-
rc.SecretAccessKey = c.SecretAccessKey
251-
}
249+
rc.SetDefaults(&c.ReplicaSettings)
252250
}
253251
}
254252
}
@@ -806,12 +804,9 @@ func ParseByteSize(s string) (int64, error) {
806804
return int64(bytes), nil
807805
}
808806

809-
// ReplicaConfig represents the configuration for a single replica in a database.
810-
type ReplicaConfig struct {
811-
Type string `yaml:"type"` // "file", "s3"
812-
Name string `yaml:"name"` // Deprecated
813-
Path string `yaml:"path"`
814-
URL string `yaml:"url"`
807+
// ReplicaSettings contains settings shared across replica configurations.
808+
// These can be set globally in Config or per-replica in ReplicaConfig.
809+
type ReplicaSettings struct {
815810
SyncInterval *time.Duration `yaml:"sync-interval"`
816811
ValidationInterval *time.Duration `yaml:"validation-interval"`
817812

@@ -865,6 +860,129 @@ type ReplicaConfig struct {
865860
} `yaml:"age"`
866861
}
867862

863+
// SetDefaults merges default settings from src into the current ReplicaSettings.
864+
// Individual settings override defaults when already set.
865+
func (rs *ReplicaSettings) SetDefaults(src *ReplicaSettings) {
866+
if src == nil {
867+
return
868+
}
869+
870+
// Timing settings
871+
if rs.SyncInterval == nil && src.SyncInterval != nil {
872+
rs.SyncInterval = src.SyncInterval
873+
}
874+
if rs.ValidationInterval == nil && src.ValidationInterval != nil {
875+
rs.ValidationInterval = src.ValidationInterval
876+
}
877+
878+
// S3 settings
879+
if rs.AccessKeyID == "" {
880+
rs.AccessKeyID = src.AccessKeyID
881+
}
882+
if rs.SecretAccessKey == "" {
883+
rs.SecretAccessKey = src.SecretAccessKey
884+
}
885+
if rs.Region == "" {
886+
rs.Region = src.Region
887+
}
888+
if rs.Bucket == "" {
889+
rs.Bucket = src.Bucket
890+
}
891+
if rs.Endpoint == "" {
892+
rs.Endpoint = src.Endpoint
893+
}
894+
if rs.ForcePathStyle == nil {
895+
rs.ForcePathStyle = src.ForcePathStyle
896+
}
897+
if src.SkipVerify {
898+
rs.SkipVerify = true
899+
}
900+
901+
// ABS settings
902+
if rs.AccountName == "" {
903+
rs.AccountName = src.AccountName
904+
}
905+
if rs.AccountKey == "" {
906+
rs.AccountKey = src.AccountKey
907+
}
908+
909+
// SFTP settings
910+
if rs.Host == "" {
911+
rs.Host = src.Host
912+
}
913+
if rs.User == "" {
914+
rs.User = src.User
915+
}
916+
if rs.Password == "" {
917+
rs.Password = src.Password
918+
}
919+
if rs.KeyPath == "" {
920+
rs.KeyPath = src.KeyPath
921+
}
922+
if rs.ConcurrentWrites == nil {
923+
rs.ConcurrentWrites = src.ConcurrentWrites
924+
}
925+
926+
// NATS settings
927+
if rs.JWT == "" {
928+
rs.JWT = src.JWT
929+
}
930+
if rs.Seed == "" {
931+
rs.Seed = src.Seed
932+
}
933+
if rs.Creds == "" {
934+
rs.Creds = src.Creds
935+
}
936+
if rs.NKey == "" {
937+
rs.NKey = src.NKey
938+
}
939+
if rs.Username == "" {
940+
rs.Username = src.Username
941+
}
942+
if rs.Token == "" {
943+
rs.Token = src.Token
944+
}
945+
if !rs.TLS {
946+
rs.TLS = src.TLS
947+
}
948+
if len(rs.RootCAs) == 0 {
949+
rs.RootCAs = src.RootCAs
950+
}
951+
if rs.ClientCert == "" {
952+
rs.ClientCert = src.ClientCert
953+
}
954+
if rs.ClientKey == "" {
955+
rs.ClientKey = src.ClientKey
956+
}
957+
if rs.MaxReconnects == nil {
958+
rs.MaxReconnects = src.MaxReconnects
959+
}
960+
if rs.ReconnectWait == nil {
961+
rs.ReconnectWait = src.ReconnectWait
962+
}
963+
if rs.Timeout == nil {
964+
rs.Timeout = src.Timeout
965+
}
966+
967+
// Age encryption settings
968+
if len(rs.Age.Identities) == 0 {
969+
rs.Age.Identities = src.Age.Identities
970+
}
971+
if len(rs.Age.Recipients) == 0 {
972+
rs.Age.Recipients = src.Age.Recipients
973+
}
974+
}
975+
976+
// ReplicaConfig represents the configuration for a single replica in a database.
977+
type ReplicaConfig struct {
978+
ReplicaSettings `yaml:",inline"`
979+
980+
Type string `yaml:"type"` // "file", "s3"
981+
Name string `yaml:"name"` // Deprecated
982+
Path string `yaml:"path"`
983+
URL string `yaml:"url"`
984+
}
985+
868986
// NewReplicaFromConfig instantiates a replica for a DB based on a config.
869987
func NewReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *litestream.Replica, err error) {
870988
// Ensure user did not specify URL in path.

0 commit comments

Comments
 (0)