Skip to content

Commit 53f896e

Browse files
corylanouclaude
andauthored
feat(s3): add automatic Tigris endpoint detection (#845)
Co-authored-by: Claude <[email protected]>
1 parent 88632bb commit 53f896e

File tree

3 files changed

+121
-21
lines changed

3 files changed

+121
-21
lines changed

cmd/litestream/main.go

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,13 +1109,13 @@ func NewS3ReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_ *s
11091109

11101110
bucket, configPath := c.Bucket, c.Path
11111111
region, endpoint, skipVerify := c.Region, c.Endpoint, c.SkipVerify
1112-
signPayload := false
1112+
signSetting := newBoolSetting(false)
11131113
if v := c.SignPayload; v != nil {
1114-
signPayload = *v
1114+
signSetting.Set(*v)
11151115
}
1116-
requireContentMD5 := true
1116+
requireSetting := newBoolSetting(true)
11171117
if v := c.RequireContentMD5; v != nil {
1118-
requireContentMD5 = *v
1118+
requireSetting.Set(*v)
11191119
}
11201120

11211121
// Use path style if an endpoint is explicitly set. This works because the
@@ -1126,21 +1126,28 @@ func NewS3ReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_ *s
11261126
}
11271127

11281128
// Apply settings from URL, if specified.
1129+
var (
1130+
endpointWasSet bool
1131+
usignPayload bool
1132+
usignPayloadSet bool
1133+
urequireContentMD5 bool
1134+
urequireContentMD5Set bool
1135+
)
1136+
if endpoint != "" {
1137+
endpointWasSet = true
1138+
}
1139+
11291140
if c.URL != "" {
11301141
_, host, upath, query, err := ParseReplicaURLWithQuery(c.URL)
11311142
if err != nil {
11321143
return nil, err
11331144
}
11341145

11351146
var (
1136-
ubucket string
1137-
uregion string
1138-
uendpoint string
1139-
uforcePathStyle bool
1140-
usignPayload bool
1141-
usignPayloadSet bool
1142-
urequireContentMD5 bool
1143-
urequireContentMD5Set bool
1147+
ubucket string
1148+
uregion string
1149+
uendpoint string
1150+
uforcePathStyle bool
11441151
)
11451152

11461153
if strings.HasPrefix(host, "arn:") {
@@ -1162,6 +1169,7 @@ func NewS3ReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_ *s
11621169
if query.Get("forcePathStyle") != "false" {
11631170
uforcePathStyle = true
11641171
}
1172+
endpointWasSet = true
11651173
}
11661174
if qRegion := query.Get("region"); qRegion != "" {
11671175
uregion = qRegion
@@ -1197,11 +1205,11 @@ func NewS3ReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_ *s
11971205
if !forcePathStyle {
11981206
forcePathStyle = uforcePathStyle
11991207
}
1200-
if c.SignPayload == nil && usignPayloadSet {
1201-
signPayload = usignPayload
1208+
if !signSetting.set && usignPayloadSet {
1209+
signSetting.Set(usignPayload)
12021210
}
1203-
if c.RequireContentMD5 == nil && urequireContentMD5Set {
1204-
requireContentMD5 = urequireContentMD5
1211+
if !requireSetting.set && urequireContentMD5Set {
1212+
requireSetting.Set(urequireContentMD5)
12051213
}
12061214
}
12071215

@@ -1210,6 +1218,11 @@ func NewS3ReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_ *s
12101218
return nil, fmt.Errorf("bucket required for s3 replica")
12111219
}
12121220

1221+
isTigris := isTigrisEndpoint(endpoint)
1222+
if !isTigris && !endpointWasSet && isTigrisEndpoint(c.Endpoint) {
1223+
isTigris = true
1224+
}
1225+
12131226
// Build replica.
12141227
client := s3.NewReplicaClient()
12151228
client.AccessKeyID = c.AccessKeyID
@@ -1220,8 +1233,13 @@ func NewS3ReplicaClientFromConfig(c *ReplicaConfig, _ *litestream.Replica) (_ *s
12201233
client.Endpoint = endpoint
12211234
client.ForcePathStyle = forcePathStyle
12221235
client.SkipVerify = skipVerify
1223-
client.SignPayload = signPayload
1224-
client.RequireContentMD5 = requireContentMD5
1236+
if isTigris {
1237+
signSetting.ApplyDefault(true)
1238+
requireSetting.ApplyDefault(false)
1239+
}
1240+
1241+
client.SignPayload = signSetting.value
1242+
client.RequireContentMD5 = requireSetting.value
12251243

12261244
// Apply upload configuration if specified.
12271245
if c.PartSize != nil {
@@ -1625,6 +1643,39 @@ func boolQueryValue(query url.Values, keys ...string) (bool, bool) {
16251643
return false, false
16261644
}
16271645

1646+
func isTigrisEndpoint(endpoint string) bool {
1647+
endpoint = strings.TrimSpace(strings.ToLower(endpoint))
1648+
if endpoint == "" {
1649+
return false
1650+
}
1651+
if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") {
1652+
if u, err := url.Parse(endpoint); err == nil && u.Host != "" {
1653+
endpoint = u.Host
1654+
}
1655+
}
1656+
return endpoint == "fly.storage.tigris.dev"
1657+
}
1658+
1659+
type boolSetting struct {
1660+
value bool
1661+
set bool
1662+
}
1663+
1664+
func newBoolSetting(defaultValue bool) boolSetting {
1665+
return boolSetting{value: defaultValue}
1666+
}
1667+
1668+
func (s *boolSetting) Set(value bool) {
1669+
s.value = value
1670+
s.set = true
1671+
}
1672+
1673+
func (s *boolSetting) ApplyDefault(value bool) {
1674+
if !s.set {
1675+
s.value = value
1676+
}
1677+
}
1678+
16281679
func regionFromS3ARN(arn string) string {
16291680
parts := strings.SplitN(arn, ":", 6)
16301681
if len(parts) >= 4 {

cmd/litestream/main_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,34 @@ func TestNewS3ReplicaClientFromConfig(t *testing.T) {
22312231
if !client.ForcePathStyle {
22322232
t.Error("expected ForcePathStyle to be true for custom endpoint")
22332233
}
2234+
if !client.SignPayload {
2235+
t.Error("expected SignPayload to be true for Tigris")
2236+
}
2237+
if client.RequireContentMD5 {
2238+
t.Error("expected RequireContentMD5 to be false for Tigris")
2239+
}
2240+
})
2241+
2242+
t.Run("TigrisConfigEndpoint", func(t *testing.T) {
2243+
config := &main.ReplicaConfig{
2244+
Path: "path",
2245+
ReplicaSettings: main.ReplicaSettings{
2246+
Bucket: "mybucket",
2247+
Endpoint: "https://fly.storage.tigris.dev",
2248+
},
2249+
}
2250+
2251+
client, err := main.NewS3ReplicaClientFromConfig(config, nil)
2252+
if err != nil {
2253+
t.Fatal(err)
2254+
}
2255+
2256+
if !client.SignPayload {
2257+
t.Error("expected SignPayload to be true for config-based Tigris endpoint")
2258+
}
2259+
if client.RequireContentMD5 {
2260+
t.Error("expected RequireContentMD5 to be false for config-based Tigris endpoint")
2261+
}
22342262
})
22352263

22362264
t.Run("HTTPSEndpoint", func(t *testing.T) {
@@ -2290,6 +2318,29 @@ func TestNewS3ReplicaClientFromConfig(t *testing.T) {
22902318
t.Error("expected config RequireContentMD5=false to override query parameter")
22912319
}
22922320
})
2321+
2322+
t.Run("TigrisManualOverride", func(t *testing.T) {
2323+
signFalse := false
2324+
requireTrue := true
2325+
config := &main.ReplicaConfig{
2326+
URL: "s3://bucket/db?endpoint=fly.storage.tigris.dev&region=auto",
2327+
ReplicaSettings: main.ReplicaSettings{
2328+
SignPayload: &signFalse,
2329+
RequireContentMD5: &requireTrue,
2330+
},
2331+
}
2332+
2333+
client, err := main.NewS3ReplicaClientFromConfig(config, nil)
2334+
if err != nil {
2335+
t.Fatal(err)
2336+
}
2337+
if client.SignPayload {
2338+
t.Error("expected manual SignPayload override to take precedence")
2339+
}
2340+
if !client.RequireContentMD5 {
2341+
t.Error("expected manual RequireContentMD5 override to take precedence")
2342+
}
2343+
})
22932344
}
22942345
func TestGlobalDefaults(t *testing.T) {
22952346
// Test comprehensive global defaults functionality

etc/litestream.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
# replica:
88
# path: /path/to/replica # File-based replication
99
# url: s3://my.bucket.com/db # S3-based replication
10-
# # Example Fly.io Tigris setup (requires signed payloads & no Content-MD5 deletes)
10+
# # Example Fly.io Tigris setup (signing/no-Content-MD5 are auto-enabled for this endpoint)
1111
# # url: s3://my-tigris-bucket/db.sqlite?endpoint=fly.storage.tigris.dev&region=auto
12-
# # sign-payload: true
13-
# # require-content-md5: false
1412
# type: nats # NATS JetStream replication
1513
# url: nats://nats.example.com:4222
1614
# bucket: litestream-backups

0 commit comments

Comments
 (0)