fix(s3): enable automatic ARN endpoint resolution for Access Points (#924)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cory LaNou
2025-12-21 08:23:25 -06:00
committed by GitHub
parent e1d5aad75b
commit 0a441d06fe
4 changed files with 320 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ go build -o bin/litestream-test ./cmd/litestream-test
| test-s3-retention-small-db.sh | S3 retention 50MB | ~8min | ✅ Stable |
| test-s3-retention-large-db.sh | S3 retention 1.5GB | ~20min | ✅ Stable |
| test-s3-retention-comprehensive.sh | Full S3 retention suite | ~30min | ✅ Stable |
| test-s3-access-point.sh | S3 Access Point ARN support | ~2min | ✅ Stable |
## Test Categories
@@ -202,6 +203,26 @@ Tests upgrade with very large databases and long-running scenarios.
For detailed S3 retention testing documentation, see [S3-RETENTION-TESTING.md](../S3-RETENTION-TESTING.md).
#### test-s3-access-point.sh
Tests S3 Access Point ARN support (Issue #923). Verifies that Access Point ARNs work automatically without manual endpoint configuration.
```bash
export LITESTREAM_S3_ACCESS_POINT_ARN='arn:aws:s3:us-east-2:123456789012:accesspoint/my-access-point'
./cmd/litestream-test/scripts/test-s3-access-point.sh
```
**Tests:**
- Replication to S3 Access Point using ARN
- Automatic endpoint resolution (UseARNRegion)
- Restore from Access Point ARN
- Data integrity verification
**Environment Variables:**
- `LITESTREAM_S3_ACCESS_POINT_ARN` - Full ARN of the S3 Access Point (required)
- `LITESTREAM_S3_REGION` - AWS region (optional, extracted from ARN)
- `LITESTREAM_S3_PREFIX` - Path prefix in bucket (optional)
- AWS credentials via standard methods (env vars, credentials file, IAM role)
#### test-s3-retention-cleanup.sh
Basic S3 LTX retention cleanup testing.

View File

@@ -0,0 +1,236 @@
#!/bin/bash
set -e
# Test S3 Access Point ARN support (Issue #923)
# This script tests that Litestream can replicate to S3 using Access Point ARNs
# without requiring manual endpoint configuration.
echo "=========================================="
echo "S3 Access Point ARN Test (Issue #923)"
echo "=========================================="
echo ""
echo "This test verifies that S3 Access Point ARNs work automatically"
echo "without requiring manual endpoint configuration."
echo ""
# Configuration
DB="/tmp/access-point-test.db"
RESTORED_DB="/tmp/access-point-restored.db"
LITESTREAM="./bin/litestream"
LOG="/tmp/access-point-test.log"
CONFIG="/tmp/access-point-config.yml"
# S3 Access Point Configuration
# The ARN format: arn:aws:s3:REGION:ACCOUNT_ID:accesspoint/ACCESS_POINT_NAME
S3_ACCESS_POINT_ARN="${LITESTREAM_S3_ACCESS_POINT_ARN:-}"
S3_PREFIX="${LITESTREAM_S3_PREFIX:-litestream-access-point-test}"
S3_REGION="${LITESTREAM_S3_REGION:-}"
# Check prerequisites
check_prerequisites() {
echo "[Prerequisites]"
if [ ! -f "$LITESTREAM" ]; then
echo "❌ Litestream binary not found at $LITESTREAM"
echo " Run: go build -o bin/litestream ./cmd/litestream"
exit 1
fi
echo " ✓ Litestream binary found"
if ! command -v sqlite3 &> /dev/null; then
echo "❌ sqlite3 not found"
exit 1
fi
echo " ✓ sqlite3 found"
if [ -z "$S3_ACCESS_POINT_ARN" ]; then
echo ""
echo "⚠️ S3 Access Point ARN not configured!"
echo ""
echo "Please set the following environment variables:"
echo ""
echo " export LITESTREAM_S3_ACCESS_POINT_ARN='arn:aws:s3:REGION:ACCOUNT:accesspoint/NAME'"
echo " export LITESTREAM_S3_REGION='us-east-1' # Optional, extracted from ARN"
echo " export LITESTREAM_S3_PREFIX='test-prefix' # Optional"
echo ""
echo "You also need AWS credentials configured via:"
echo " - Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)"
echo " - AWS credentials file (~/.aws/credentials)"
echo " - IAM role (if running on AWS)"
echo ""
echo "Example:"
echo " export LITESTREAM_S3_ACCESS_POINT_ARN='arn:aws:s3:us-east-2:123456789012:accesspoint/my-access-point'"
echo " ./cmd/litestream-test/scripts/test-s3-access-point.sh"
echo ""
exit 1
fi
echo " ✓ Access Point ARN configured"
# Extract region from ARN if not explicitly set
if [ -z "$S3_REGION" ]; then
# ARN format: arn:aws:s3:REGION:ACCOUNT:accesspoint/NAME
S3_REGION=$(echo "$S3_ACCESS_POINT_ARN" | cut -d: -f4)
echo " ✓ Region extracted from ARN: $S3_REGION"
fi
echo ""
echo "Configuration:"
echo " Access Point ARN: $S3_ACCESS_POINT_ARN"
echo " Region: $S3_REGION"
echo " Prefix: $S3_PREFIX"
echo ""
}
# Cleanup function
cleanup() {
echo ""
echo "[Cleanup]"
pkill -f "litestream replicate.*access-point-test" 2>/dev/null || true
rm -f "$DB"* "$RESTORED_DB"* "$LOG" "$CONFIG"
echo " ✓ Cleaned up test artifacts"
}
trap cleanup EXIT
# Run prerequisites check
check_prerequisites
# Clean up any previous test artifacts
cleanup 2>/dev/null || true
echo "=========================================="
echo "Test 1: Replication to Access Point ARN"
echo "=========================================="
echo ""
echo "[1] Creating test database..."
sqlite3 "$DB" <<EOF
PRAGMA journal_mode = WAL;
CREATE TABLE access_point_test (
id INTEGER PRIMARY KEY,
data TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO access_point_test (data) SELECT 'initial data ' || value FROM generate_series(1, 100);
EOF
echo " ✓ Database created with 100 rows"
# Create config using Access Point ARN WITHOUT explicit endpoint
# This is the key test - it should work automatically with UseARNRegion=true
echo "[2] Creating config with Access Point ARN (no explicit endpoint)..."
cat > "$CONFIG" <<EOF
dbs:
- path: $DB
replicas:
- type: s3
bucket: $S3_ACCESS_POINT_ARN
path: $S3_PREFIX
region: $S3_REGION
sync-interval: 1s
EOF
echo " ✓ Config created"
echo ""
echo " Config contents:"
cat "$CONFIG" | sed 's/^/ /'
echo ""
echo "[3] Starting replication..."
$LITESTREAM replicate -config "$CONFIG" > "$LOG" 2>&1 &
REPL_PID=$!
echo " ✓ Litestream started (PID: $REPL_PID)"
# Wait for initial sync
echo "[4] Waiting for initial sync (10 seconds)..."
sleep 10
# Check if Litestream is still running
if ! kill -0 $REPL_PID 2>/dev/null; then
echo "❌ Litestream exited unexpectedly!"
echo ""
echo "Log output:"
cat "$LOG" | sed 's/^/ /'
exit 1
fi
echo " ✓ Litestream still running"
echo "[5] Adding more data..."
sqlite3 "$DB" <<EOF
INSERT INTO access_point_test (data) SELECT 'batch 2 data ' || value FROM generate_series(1, 100);
EOF
echo " ✓ Added 100 more rows"
# Wait for sync
echo "[6] Waiting for sync (5 seconds)..."
sleep 5
# Stop replication
echo "[7] Stopping replication..."
kill $REPL_PID 2>/dev/null || true
wait $REPL_PID 2>/dev/null || true
echo " ✓ Litestream stopped"
# Check for errors in log
echo "[8] Checking for errors in log..."
if grep -qi "error\|fail\|403\|AccessDenied" "$LOG"; then
echo "❌ Errors found in log!"
echo ""
echo "Log output:"
grep -i "error\|fail\|403\|AccessDenied" "$LOG" | sed 's/^/ /'
echo ""
echo "Full log:"
cat "$LOG" | sed 's/^/ /'
exit 1
fi
echo " ✓ No errors in log"
echo ""
echo "=========================================="
echo "Test 2: Restore from Access Point ARN"
echo "=========================================="
echo ""
echo "[1] Restoring database from Access Point..."
RESTORE_URL="s3://${S3_ACCESS_POINT_ARN}/${S3_PREFIX}"
echo " Restore URL: $RESTORE_URL"
if ! $LITESTREAM restore -o "$RESTORED_DB" "$RESTORE_URL" 2>&1; then
echo "❌ Restore failed!"
exit 1
fi
echo " ✓ Restore completed"
echo "[2] Verifying restored data..."
ORIGINAL_COUNT=$(sqlite3 "$DB" "SELECT COUNT(*) FROM access_point_test")
RESTORED_COUNT=$(sqlite3 "$RESTORED_DB" "SELECT COUNT(*) FROM access_point_test")
echo " Original rows: $ORIGINAL_COUNT"
echo " Restored rows: $RESTORED_COUNT"
if [ "$ORIGINAL_COUNT" != "$RESTORED_COUNT" ]; then
echo "❌ Row count mismatch!"
exit 1
fi
echo " ✓ Row counts match"
echo "[3] Running integrity check..."
INTEGRITY=$(sqlite3 "$RESTORED_DB" "PRAGMA integrity_check")
if [ "$INTEGRITY" != "ok" ]; then
echo "❌ Integrity check failed: $INTEGRITY"
exit 1
fi
echo " ✓ Integrity check passed"
echo ""
echo "=========================================="
echo "✅ All Tests Passed!"
echo "=========================================="
echo ""
echo "S3 Access Point ARN works correctly without manual endpoint configuration."
echo "The UseARNRegion=true fix (Issue #923) is working as expected."
echo ""
echo "Test Summary:"
echo " - Replication to Access Point ARN: ✓"
echo " - Automatic endpoint resolution: ✓"
echo " - Restore from Access Point ARN: ✓"
echo " - Data integrity: ✓"
echo ""

View File

@@ -312,6 +312,7 @@ func (c *ReplicaClient) Init(ctx context.Context) (err error) {
s3Opts := []func(*s3.Options){
func(o *s3.Options) {
o.UsePathStyle = c.ForcePathStyle
o.UseARNRegion = true
// Add User-Agent and optional middleware.
o.APIOptions = append(o.APIOptions, c.middlewareOption())
},

View File

@@ -1017,6 +1017,68 @@ func TestParseHost(t *testing.T) {
}
}
func TestReplicaClient_AccessPointARN(t *testing.T) {
t.Run("ARNAsBucketName", func(t *testing.T) {
arn := "arn:aws:s3:us-east-2:123456789012:accesspoint/my-access-point"
c := NewReplicaClient()
c.Bucket = arn
c.Region = "us-east-2"
c.AccessKeyID = "test-access-key"
c.SecretAccessKey = "test-secret-key"
if c.Bucket != arn {
t.Errorf("expected bucket to be ARN, got %s", c.Bucket)
}
if c.Region != "us-east-2" {
t.Errorf("expected region to be us-east-2, got %s", c.Region)
}
})
t.Run("ARNWithPath", func(t *testing.T) {
arn := "arn:aws:s3:us-west-2:111122223333:accesspoint/prod-access-point"
c := NewReplicaClient()
c.Bucket = arn
c.Path = "my-db/replica"
c.Region = "us-west-2"
if c.Bucket != arn {
t.Errorf("expected bucket to be ARN, got %s", c.Bucket)
}
if c.Path != "my-db/replica" {
t.Errorf("expected path to be my-db/replica, got %s", c.Path)
}
})
t.Run("ARNRejectsPathStyle", func(t *testing.T) {
arn := "arn:aws:s3:us-east-1:123456789012:accesspoint/test-ap"
c := NewReplicaClient()
c.Bucket = arn
c.Path = "replica"
c.Region = "us-east-1"
c.Endpoint = "http://localhost:9000"
c.ForcePathStyle = true
c.AccessKeyID = "test-access-key"
c.SecretAccessKey = "test-secret-key"
ctx := context.Background()
if err := c.Init(ctx); err != nil {
t.Fatalf("Init() with ARN bucket should not fail: %v", err)
}
data := mustLTX(t)
_, err := c.WriteLTXFile(ctx, 0, 2, 2, bytes.NewReader(data))
if err == nil {
t.Fatal("expected error when using path-style with ARN bucket")
}
if !strings.Contains(err.Error(), "Path-style addressing cannot be used with ARN") {
t.Errorf("expected path-style ARN error, got: %v", err)
}
})
}
func TestReplicaClient_TigrisConsistentHeader(t *testing.T) {
// Test that non-Tigris endpoints do NOT send the X-Tigris-Consistent header.
// The Tigris case (header sent) requires an actual Tigris endpoint and is