Add Google Cloud Storage replica
This commit is contained in:
@@ -27,6 +27,9 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// ReplicaClientType is the client type for this package.
|
||||
const ReplicaClientType = "s3"
|
||||
|
||||
// MaxKeys is the number of keys S3 can operate on per batch.
|
||||
const MaxKeys = 1000
|
||||
|
||||
@@ -61,7 +64,7 @@ func NewReplicaClient() *ReplicaClient {
|
||||
|
||||
// Type returns "s3" as the client type.
|
||||
func (c *ReplicaClient) Type() string {
|
||||
return "s3"
|
||||
return ReplicaClientType
|
||||
}
|
||||
|
||||
// Init initializes the connection to S3. No-op if already initialized.
|
||||
@@ -181,7 +184,7 @@ func (c *ReplicaClient) DeleteGeneration(ctx context.Context, generation string)
|
||||
|
||||
dir, err := litestream.GenerationPath(c.Path, generation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine generation directory path: %w", err)
|
||||
return fmt.Errorf("cannot determine generation path: %w", err)
|
||||
}
|
||||
|
||||
// Collect all files for the generation.
|
||||
@@ -499,7 +502,7 @@ func (itr *snapshotIterator) fetch() error {
|
||||
|
||||
dir, err := litestream.SnapshotsPath(itr.client.Path, itr.generation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine snapshot directory path: %w", err)
|
||||
return fmt.Errorf("cannot determine snapshots path: %w", err)
|
||||
}
|
||||
|
||||
return itr.client.s3.ListObjectsPagesWithContext(itr.ctx, &s3.ListObjectsInput{
|
||||
@@ -602,7 +605,7 @@ func (itr *walSegmentIterator) fetch() error {
|
||||
|
||||
dir, err := litestream.WALPath(itr.client.Path, itr.generation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine wal directory path: %w", err)
|
||||
return fmt.Errorf("cannot determine wal path: %w", err)
|
||||
}
|
||||
|
||||
return itr.client.s3.ListObjectsPagesWithContext(itr.ctx, &s3.ListObjectsInput{
|
||||
@@ -691,9 +694,6 @@ func ParseHost(s string) (bucket, region, endpoint string, forcePathStyle bool)
|
||||
if a := localhostRegex.FindStringSubmatch(host); a != nil {
|
||||
bucket, region = a[1], "us-east-1"
|
||||
scheme, endpoint = "http", "localhost"
|
||||
} else if a := gcsRegex.FindStringSubmatch(host); a != nil {
|
||||
bucket, region = a[1], "us-east-1"
|
||||
endpoint = "storage.googleapis.com"
|
||||
} else if a := digitalOceanRegex.FindStringSubmatch(host); a != nil {
|
||||
bucket, region = a[1], a[2]
|
||||
endpoint = fmt.Sprintf("%s.digitaloceanspaces.com", region)
|
||||
@@ -726,7 +726,6 @@ var (
|
||||
digitalOceanRegex = regexp.MustCompile(`^(?:(.+)\.)?([^.]+)\.digitaloceanspaces.com$`)
|
||||
linodeRegex = regexp.MustCompile(`^(?:(.+)\.)?([^.]+)\.linodeobjects.com$`)
|
||||
backblazeRegex = regexp.MustCompile(`^(?:(.+)\.)?s3.([^.]+)\.backblazeb2.com$`)
|
||||
gcsRegex = regexp.MustCompile(`^(?:(.+)\.)?storage.googleapis.com$`)
|
||||
)
|
||||
|
||||
func isNotExists(err error) bool {
|
||||
|
||||
@@ -1,578 +0,0 @@
|
||||
package s3_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/benbjohnson/litestream"
|
||||
"github.com/benbjohnson/litestream/s3"
|
||||
)
|
||||
|
||||
var (
|
||||
// Enables integration tests.
|
||||
integration = flag.Bool("integration", false, "")
|
||||
|
||||
// Replica client settings
|
||||
accessKeyID = flag.String("access-key-id", os.Getenv("LITESTREAM_S3_ACCESS_KEY_ID"), "")
|
||||
secretAccessKey = flag.String("secret-access-key", os.Getenv("LITESTREAM_S3_SECRET_ACCESS_KEY"), "")
|
||||
region = flag.String("region", os.Getenv("LITESTREAM_S3_REGION"), "")
|
||||
bucket = flag.String("bucket", os.Getenv("LITESTREAM_S3_BUCKET"), "")
|
||||
pathFlag = flag.String("path", os.Getenv("LITESTREAM_S3_PATH"), "")
|
||||
endpoint = flag.String("endpoint", os.Getenv("LITESTREAM_S3_ENDPOINT"), "")
|
||||
forcePathStyle = flag.Bool("force-path-style", os.Getenv("LITESTREAM_S3_FORCE_PATH_STYLE") == "true", "")
|
||||
skipVerify = flag.Bool("skip-verify", os.Getenv("LITESTREAM_S3_SKIP_VERIFY") == "true", "")
|
||||
)
|
||||
|
||||
func TestReplicaClient_Type(t *testing.T) {
|
||||
if got, want := s3.NewReplicaClient().Type(), "s3"; got != want {
|
||||
t.Fatalf("Type()=%v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplicaClient_Generations(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
// Write snapshots.
|
||||
if _, err := c.WriteSnapshot(context.Background(), "5efbd8d042012dca", 0, strings.NewReader(`foo`)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c.WriteSnapshot(context.Background(), "b16ddcf5c697540f", 0, strings.NewReader(`bar`)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c.WriteSnapshot(context.Background(), "155fe292f8333c72", 0, strings.NewReader(`baz`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify returned generations.
|
||||
if got, err := c.Generations(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := []string{"155fe292f8333c72", "5efbd8d042012dca", "b16ddcf5c697540f"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Generations()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoGenerationsDir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
if generations, err := c.Generations(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := len(generations), 0; got != want {
|
||||
t.Fatalf("len(Generations())=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_Snapshots(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
// Write snapshots.
|
||||
if _, err := c.WriteSnapshot(context.Background(), "5efbd8d042012dca", 1, strings.NewReader(``)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c.WriteSnapshot(context.Background(), "b16ddcf5c697540f", 5, strings.NewReader(`x`)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c.WriteSnapshot(context.Background(), "b16ddcf5c697540f", 10, strings.NewReader(`xyz`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fetch all snapshots by generation.
|
||||
itr, err := c.Snapshots(context.Background(), "b16ddcf5c697540f")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
// Read all snapshots into a slice so they can be sorted.
|
||||
a, err := litestream.SliceSnapshotIterator(itr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := len(a), 2; got != want {
|
||||
t.Fatalf("len=%v, want %v", got, want)
|
||||
}
|
||||
sort.Sort(litestream.SnapshotInfoSlice(a))
|
||||
|
||||
// Verify first snapshot metadata.
|
||||
if got, want := a[0].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Index, 5; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Size, int64(1); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[0].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Verify second snapshot metadata.
|
||||
if got, want := a[1].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Index, 0xA; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Size, int64(3); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[1].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Ensure close is clean.
|
||||
if err := itr.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoGenerationDir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
itr, err := c.Snapshots(context.Background(), "5efbd8d042012dca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
if itr.Next() {
|
||||
t.Fatal("expected no snapshots")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if itr, err := c.Snapshots(context.Background(), ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := itr.Close(); err == nil || err.Error() != `cannot determine snapshot directory path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WriteSnapshot(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WriteSnapshot(context.Background(), "b16ddcf5c697540f", 1000, strings.NewReader(`foobar`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r, err := c.SnapshotReader(context.Background(), "b16ddcf5c697540f", 1000); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if buf, err := ioutil.ReadAll(r); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := r.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := string(buf), `foobar`; got != want {
|
||||
t.Fatalf("data=%q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := NewIntegrationReplicaClient(t).WriteSnapshot(context.Background(), "", 0, nil); err == nil || err.Error() != `cannot determine snapshot path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_SnapshotReader(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WriteSnapshot(context.Background(), "5efbd8d042012dca", 10, strings.NewReader(`foo`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err := c.SnapshotReader(context.Background(), "5efbd8d042012dca", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if buf, err := ioutil.ReadAll(r); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := string(buf), "foo"; got != want {
|
||||
t.Fatalf("ReadAll=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.SnapshotReader(context.Background(), "5efbd8d042012dca", 1); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not exist, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrGeneration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.SnapshotReader(context.Background(), "", 1); err == nil || err.Error() != `cannot determine snapshot path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WALs(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 1, Offset: 0}, strings.NewReader(``)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 2, Offset: 0}, strings.NewReader(`12345`)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 2, Offset: 5}, strings.NewReader(`67`)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 3, Offset: 0}, strings.NewReader(`xyz`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
itr, err := c.WALSegments(context.Background(), "b16ddcf5c697540f")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
// Read all WAL segment files into a slice so they can be sorted.
|
||||
a, err := litestream.SliceWALSegmentIterator(itr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := len(a), 3; got != want {
|
||||
t.Fatalf("len=%v, want %v", got, want)
|
||||
}
|
||||
sort.Sort(litestream.WALSegmentInfoSlice(a))
|
||||
|
||||
// Verify first WAL segment metadata.
|
||||
if got, want := a[0].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Index, 2; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Offset, int64(0); got != want {
|
||||
t.Fatalf("Offset=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Size, int64(5); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[0].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Verify first WAL segment metadata.
|
||||
if got, want := a[1].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Index, 2; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Offset, int64(5); got != want {
|
||||
t.Fatalf("Offset=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Size, int64(2); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[1].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Verify third WAL segment metadata.
|
||||
if got, want := a[2].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[2].Index, 3; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[2].Offset, int64(0); got != want {
|
||||
t.Fatalf("Offset=%v, want %v", got, want)
|
||||
} else if got, want := a[2].Size, int64(3); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[1].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Ensure close is clean.
|
||||
if err := itr.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoGenerationDir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
itr, err := c.WALSegments(context.Background(), "5efbd8d042012dca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
if itr.Next() {
|
||||
t.Fatal("expected no wal files")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoWALs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WriteSnapshot(context.Background(), "5efbd8d042012dca", 0, strings.NewReader(`foo`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
itr, err := c.WALSegments(context.Background(), "5efbd8d042012dca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
if itr.Next() {
|
||||
t.Fatal("expected no wal files")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if itr, err := c.WALSegments(context.Background(), ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := itr.Close(); err == nil || err.Error() != `cannot determine wal directory path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WriteWALSegment(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 1000, Offset: 2000}, strings.NewReader(`foobar`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 1000, Offset: 2000}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if buf, err := ioutil.ReadAll(r); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := r.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := string(buf), `foobar`; got != want {
|
||||
t.Fatalf("data=%q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := NewIntegrationReplicaClient(t).WriteWALSegment(context.Background(), litestream.Pos{Generation: "", Index: 0, Offset: 0}, nil); err == nil || err.Error() != `cannot determine wal segment path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WALReader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 10, Offset: 5}, strings.NewReader(`foobar`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
r, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 10, Offset: 5})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if buf, err := ioutil.ReadAll(r); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := string(buf), "foobar"; got != want {
|
||||
t.Fatalf("ReadAll=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 1, Offset: 0}); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not exist, got %#v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_DeleteWALSegments(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := NewIntegrationReplicaClient(t)
|
||||
defer MustDeleteAll(t, c)
|
||||
|
||||
if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 1, Offset: 2}, strings.NewReader(`foo`)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 3, Offset: 4}, strings.NewReader(`bar`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := c.DeleteWALSegments(context.Background(), []litestream.Pos{
|
||||
{Generation: "b16ddcf5c697540f", Index: 1, Offset: 2},
|
||||
{Generation: "5efbd8d042012dca", Index: 3, Offset: 4},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 1, Offset: 2}); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not exist, got %#v", err)
|
||||
} else if _, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 3, Offset: 4}); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not exist, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := NewIntegrationReplicaClient(t).DeleteWALSegments(context.Background(), []litestream.Pos{{}}); err == nil || err.Error() != `cannot determine wal segment path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseHost(t *testing.T) {
|
||||
// Ensure non-specific hosts return as buckets.
|
||||
t.Run("S3", func(t *testing.T) {
|
||||
bucket, region, endpoint, forcePathStyle := s3.ParseHost(`test.litestream.io`)
|
||||
if got, want := bucket, `test.litestream.io`; got != want {
|
||||
t.Fatalf("bucket=%q, want %q", got, want)
|
||||
} else if got, want := region, ``; got != want {
|
||||
t.Fatalf("region=%q, want %q", got, want)
|
||||
} else if got, want := endpoint, ``; got != want {
|
||||
t.Fatalf("endpoint=%q, want %q", got, want)
|
||||
} else if got, want := forcePathStyle, false; got != want {
|
||||
t.Fatalf("forcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure localhosts use an HTTP endpoint and extract the bucket name.
|
||||
t.Run("Localhost", func(t *testing.T) {
|
||||
t.Run("WithPort", func(t *testing.T) {
|
||||
bucket, region, endpoint, forcePathStyle := s3.ParseHost(`test.localhost:9000`)
|
||||
if got, want := bucket, `test`; got != want {
|
||||
t.Fatalf("bucket=%q, want %q", got, want)
|
||||
} else if got, want := region, `us-east-1`; got != want {
|
||||
t.Fatalf("region=%q, want %q", got, want)
|
||||
} else if got, want := endpoint, `http://localhost:9000`; got != want {
|
||||
t.Fatalf("endpoint=%q, want %q", got, want)
|
||||
} else if got, want := forcePathStyle, true; got != want {
|
||||
t.Fatalf("forcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WithoutPort", func(t *testing.T) {
|
||||
bucket, region, endpoint, forcePathStyle := s3.ParseHost(`test.localhost`)
|
||||
if got, want := bucket, `test`; got != want {
|
||||
t.Fatalf("bucket=%q, want %q", got, want)
|
||||
} else if got, want := region, `us-east-1`; got != want {
|
||||
t.Fatalf("region=%q, want %q", got, want)
|
||||
} else if got, want := endpoint, `http://localhost`; got != want {
|
||||
t.Fatalf("endpoint=%q, want %q", got, want)
|
||||
} else if got, want := forcePathStyle, true; got != want {
|
||||
t.Fatalf("forcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Ensure backblaze B2 URLs extract bucket, region, & endpoint from host.
|
||||
t.Run("Backblaze", func(t *testing.T) {
|
||||
bucket, region, endpoint, forcePathStyle := s3.ParseHost(`test-123.s3.us-west-000.backblazeb2.com`)
|
||||
if got, want := bucket, `test-123`; got != want {
|
||||
t.Fatalf("bucket=%q, want %q", got, want)
|
||||
} else if got, want := region, `us-west-000`; got != want {
|
||||
t.Fatalf("region=%q, want %q", got, want)
|
||||
} else if got, want := endpoint, `https://s3.us-west-000.backblazeb2.com`; got != want {
|
||||
t.Fatalf("endpoint=%q, want %q", got, want)
|
||||
} else if got, want := forcePathStyle, true; got != want {
|
||||
t.Fatalf("forcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure GCS URLs extract bucket & endpoint from host.
|
||||
t.Run("GCS", func(t *testing.T) {
|
||||
bucket, region, endpoint, forcePathStyle := s3.ParseHost(`litestream.io.storage.googleapis.com`)
|
||||
if got, want := bucket, `litestream.io`; got != want {
|
||||
t.Fatalf("bucket=%q, want %q", got, want)
|
||||
} else if got, want := region, `us-east-1`; got != want {
|
||||
t.Fatalf("region=%q, want %q", got, want)
|
||||
} else if got, want := endpoint, `https://storage.googleapis.com`; got != want {
|
||||
t.Fatalf("endpoint=%q, want %q", got, want)
|
||||
} else if got, want := forcePathStyle, true; got != want {
|
||||
t.Fatalf("forcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NewIntegrationReplicaClient returns a new client for integration testing.
|
||||
// If integration flag is not set then test/benchmark is skipped.
|
||||
func NewIntegrationReplicaClient(tb testing.TB) *s3.ReplicaClient {
|
||||
tb.Helper()
|
||||
|
||||
if !*integration {
|
||||
tb.Skip("integration tests disabled")
|
||||
}
|
||||
|
||||
c := s3.NewReplicaClient()
|
||||
c.AccessKeyID = *accessKeyID
|
||||
c.SecretAccessKey = *secretAccessKey
|
||||
c.Region = *region
|
||||
c.Bucket = *bucket
|
||||
c.Path = path.Join(*pathFlag, fmt.Sprintf("%016x", rand.Uint64()))
|
||||
c.Endpoint = *endpoint
|
||||
c.ForcePathStyle = *forcePathStyle
|
||||
c.SkipVerify = *skipVerify
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// MustDeleteAll deletes all objects under the client's path.
|
||||
func MustDeleteAll(tb testing.TB, c *s3.ReplicaClient) {
|
||||
tb.Helper()
|
||||
if err := c.DeleteAll(context.Background()); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user