Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Meilisearch Support #54

Merged
merged 54 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
dc98028
Initial Meilisearch Support
majst01 Aug 24, 2023
3ae5995
linter fix
majst01 Aug 24, 2023
6915783
linter fix
majst01 Aug 24, 2023
479027a
logging
majst01 Aug 24, 2023
95d3911
stupid me
majst01 Aug 24, 2023
86d4b9e
Fix dump move
majst01 Aug 24, 2023
4a8a7b6
Fix cross mountpoint move
majst01 Aug 24, 2023
a9f1079
Try native move
majst01 Aug 24, 2023
2b65b56
create latest stable dump for upgrades
majst01 Aug 24, 2023
408d73b
Upgrade
majst01 Aug 24, 2023
98abd10
fix lateststableDump
majst01 Aug 24, 2023
8e72438
fix lateststableDump
majst01 Aug 24, 2023
0241268
fix lateststableDump
majst01 Aug 24, 2023
d55a498
fix lateststableDump
majst01 Aug 24, 2023
603093e
semver comparison
majst01 Aug 24, 2023
bc3aac8
Ignore existing db
majst01 Aug 24, 2023
f90e1d0
Fix upgrade
majst01 Aug 24, 2023
562523c
Fix upgrade
majst01 Aug 24, 2023
4576437
Fix upgrade
majst01 Aug 24, 2023
a335f66
Fix upgrade
majst01 Aug 24, 2023
925f6a5
Kill database after upgrade
majst01 Aug 24, 2023
17e9489
Kill database after upgrade
majst01 Aug 24, 2023
2171467
Errgroup
majst01 Aug 25, 2023
6e0e9aa
fixes
majst01 Aug 25, 2023
99daca5
refinements
majst01 Aug 25, 2023
9fcff68
simplification and implement recover
majst01 Aug 25, 2023
884be8c
dump creation fix
majst01 Aug 25, 2023
8fadb33
Merge branch 'master' of https://github.com/metal-stack/backup-restor…
majst01 Aug 25, 2023
28193e0
Fix backup copy
majst01 Aug 25, 2023
da7cc39
Fix backup copy
majst01 Aug 25, 2023
6fc7fc9
Fix backup copy
majst01 Aug 25, 2023
e6598ac
Merge master
majst01 Sep 4, 2023
b7b48ab
Larger linting timeout
majst01 Sep 4, 2023
ab9adc2
Initial integration test
majst01 Sep 6, 2023
fd2a4a8
Linter fix
majst01 Sep 7, 2023
f4283bd
Make Meilisearch upgrade pass
majst01 Sep 7, 2023
74b64d7
Update modules
majst01 Sep 7, 2023
db5fec9
Raise integration test timeout
majst01 Sep 7, 2023
3c444a9
Do not change unrelated
majst01 Sep 8, 2023
b1d4da7
Obsolet manifest
majst01 Sep 8, 2023
09fea55
First round of review comments
majst01 Sep 12, 2023
943ba85
Make CI pass again
majst01 Sep 13, 2023
9a934f8
Better explanation of the bad situation
majst01 Sep 13, 2023
11e6aa8
Move examples to separate package and enable generating them without …
Gerrit91 Sep 18, 2023
7f0dc84
First version of upgrade by saving previous binary, also bug fixes.
Gerrit91 Sep 18, 2023
c076b7c
Stabilize integration tests.
Gerrit91 Sep 18, 2023
cea623a
Merge remote-tracking branch 'origin/master' into meilisearch-support
Gerrit91 Sep 18, 2023
07b4efd
Typo.
Gerrit91 Sep 18, 2023
a384aeb
Improvements on example generation.
Gerrit91 Sep 19, 2023
9390f6f
Another review with some improvements.
Sep 19, 2023
aa2cc72
Better wait for restore.
Sep 19, 2023
d39e57b
Reformat table.
Sep 19, 2023
423ce08
Remove unnecessary linter exclusion.
Sep 19, 2023
5988ef2
Add validation.
Gerrit91 Sep 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
- name: Lint
uses: golangci/golangci-lint-action@v3
with:
args: --build-tags integration -p bugs -p unused --timeout=3m
args: --build-tags integration -p bugs -p unused --timeout=10m

- name: Test
run: |
Expand Down
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ GO_RUN_ARG := -run $(GO_RUN)
endif

.PHONY: build
build:
build: generate-examples
go mod tidy
go build -ldflags "$(LINKMODE)" -tags 'osusergo netgo static_build' -o bin/backup-restore-sidecar github.com/metal-stack/backup-restore-sidecar/cmd
strip bin/backup-restore-sidecar
Expand All @@ -31,10 +31,14 @@ build:
test: build
go test -cover ./...

.PHONY: generate-examples
generate-examples:
go run ./pkg/generate/examples/dump.go

.PHONY: test-integration
test-integration: kind-cluster-create
kind --name backup-restore-sidecar load docker-image ghcr.io/metal-stack/backup-restore-sidecar:latest
KUBECONFIG=$(KUBECONFIG) go test $(GO_RUN_ARG) -tags=integration -count 1 -v -p 1 -timeout 10m ./...
KUBECONFIG=$(KUBECONFIG) go test $(GO_RUN_ARG) -tags=integration -count 1 -v -p 1 -timeout 20m ./...

.PHONY: proto
proto:
Expand All @@ -60,6 +64,10 @@ start-rethinkdb:
start-etcd:
$(MAKE) start DB=etcd

.PHONY: start-meilisearch
start-meilisearch:
$(MAKE) start DB=meilisearch

.PHONY: start
start: kind-cluster-create
kind --name backup-restore-sidecar load docker-image ghcr.io/metal-stack/backup-restore-sidecar:latest
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ Probably, it does not make sense to use this project with large databases. Howev

## Supported Databases

| Database | Image | Status | Upgrade Support |
| --------- | ------------ | :----: | :-------------: |
| postgres | >= 12-alpine | beta | ✅ |
| rethinkdb | >= 2.4.0 | beta | ❌ |
| ETCD | >= 3.5 | alpha | ❌ |
| Database | Image | Status | Upgrade Support |
|-------------|--------------|:------:|:---------------:|
| postgres | >= 12-alpine | beta | ✅ |
| rethinkdb | >= 2.4.0 | beta | ❌ |
| ETCD | >= 3.5 | alpha | ❌ |
| meilisearch | >= 1.2.0 | alpha | ✅ |
Gerrit91 marked this conversation as resolved.
Show resolved Hide resolved

## Database Upgrades

Expand Down
237 changes: 237 additions & 0 deletions cmd/internal/database/meilisearch/meilisearch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package meilisearch

import (
"context"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/avast/retry-go/v4"
"github.com/meilisearch/meilisearch-go"
"github.com/spf13/afero"
"golang.org/x/sync/errgroup"

"github.com/metal-stack/backup-restore-sidecar/cmd/internal/utils"
"github.com/metal-stack/backup-restore-sidecar/pkg/constants"
"go.uber.org/zap"
)

const (
meilisearchCmd = "meilisearch"
meilisearchVersionFile = "VERSION"
meilisearchDBDir = "data.ms"
latestStableDump = "latest.dump"
)

// Meilisearch implements the database interface
type Meilisearch struct {
log *zap.SugaredLogger
executor *utils.CmdExecutor
datadir string
copyBinaryAfterBackup bool

apikey string
client *meilisearch.Client
}

// New instantiates a new meilisearch database
func New(log *zap.SugaredLogger, datadir string, url string, apikey string) (*Meilisearch, error) {
client := meilisearch.NewClient(meilisearch.ClientConfig{
Host: url,
APIKey: apikey,
})

return &Meilisearch{
log: log,
datadir: datadir,
apikey: apikey,
executor: utils.NewExecutor(log),
client: client,
copyBinaryAfterBackup: true,
}, nil
}

// Backup takes a dump of meilisearch with the meilisearch client.
func (db *Meilisearch) Backup(ctx context.Context) error {
if err := os.RemoveAll(constants.BackupDir); err != nil {
return fmt.Errorf("could not clean backup directory: %w", err)
}

if err := os.MkdirAll(constants.BackupDir, 0777); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this too broad? Would 755 or something similar be enough?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably yes, but there are many other places where same permissions are used. Maybe we should create a separate PR and address this.

return fmt.Errorf("could not create backup directory: %w", err)
}

dumpResponse, err := db.client.CreateDump()
if err != nil {
return fmt.Errorf("could not create a dump: %w", err)
}

db.log.Infow("dump creation triggered", "taskUUID", dumpResponse.TaskUID)

dumpTask, err := db.client.WaitForTask(dumpResponse.TaskUID, meilisearch.WaitParams{Context: ctx})
if err != nil {
return err
}
db.log.Infow("dump created successfully", "duration", dumpTask.Duration)

dumps, err := filepath.Glob(constants.BackupDir + "/*.dump")
if err != nil {
return fmt.Errorf("unable to find dump: %w", err)
}
if len(dumps) == 0 {
return fmt.Errorf("did not find unique dump, found %d", len(dumps))
}

err = utils.Copy(afero.NewOsFs(), dumps[0], path.Join(constants.BackupDir, latestStableDump))
if err != nil {
return fmt.Errorf("unable to move dump to latest: %w", err)
}

err = os.Remove(dumps[0])
if err != nil {
return fmt.Errorf("unable to clean up dump: %w", err)
}

db.log.Debugw("successfully took backup of meilisearch")

if db.copyBinaryAfterBackup {
// for a future upgrade, the current meilisearch binary is required
err = db.copyMeilisearchBinary(ctx, true)
if err != nil {
return err
}
}

return nil
}

// Check checks whether a backup needs to be restored or not, returns true if it needs a backup
func (db *Meilisearch) Check(_ context.Context) (bool, error) {
empty, err := utils.IsEmpty(db.datadir)
if err != nil {
return false, err
}

if empty {
db.log.Info("data directory is empty")
return true, err
}

return false, nil
}

// Probe figures out if the database is running and available for taking backups.
func (db *Meilisearch) Probe(_ context.Context) error {
_, err := db.client.Version()
if err != nil {
return fmt.Errorf("connection error: %w", err)
}

healthy := db.client.IsHealthy()
if !healthy {
return fmt.Errorf("meilisearch does not report healthiness")
}

return nil
}

// Recover restores a database backup
func (db *Meilisearch) Recover(ctx context.Context) error {
dump := path.Join(constants.RestoreDir, latestStableDump)

if _, err := os.Stat(dump); os.IsNotExist(err) {
return fmt.Errorf("restore file not present: %s", dump)
}

if err := utils.RemoveContents(db.datadir); err != nil {
return fmt.Errorf("could not clean database data directory: %w", err)
}

start := time.Now()

err := db.importDump(ctx, dump)
if err != nil {
return fmt.Errorf("unable to recover %w", err)
}

db.log.Infow("recovery done", "duration", time.Since(start))

return nil
}

func (db *Meilisearch) importDump(ctx context.Context, dump string) error {
var (
err error
g, _ = errgroup.WithContext(ctx)

handleFailedRecovery = func(restoreErr error) error {
db.log.Errorw("trying to handle failed database recovery", "error", restoreErr)

if err := os.RemoveAll(db.datadir); err != nil {
db.log.Errorw("unable to cleanup database data directory after failed recovery attempt, high risk of starting with fresh database on container restart", "err", err)
} else {
db.log.Info("cleaned up database data directory after failed recovery attempt to prevent start of fresh database")
}

return restoreErr
}
)

args := []string{"--import-dump", dump, "--master-key", db.apikey, "--dump-dir", constants.RestoreDir, "--db-path", db.datadir, "--http-addr", "localhost:1"}
cmd := exec.CommandContext(ctx, meilisearchCmd, args...) // nolint:gosec
Gerrit91 marked this conversation as resolved.
Show resolved Hide resolved
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

g.Go(func() error {
db.log.Infow("execute meilisearch", "args", args)

err = cmd.Run()
if err != nil {
return err
}

db.log.Info("import of dump finished")

return nil
})

// TODO big databases might take longer, not sure if 100 attempts are enough
// must check how long it take max with backoff ?
err = retry.Do(func() error {
restoreDB, err := New(db.log, db.datadir, "http://localhost:1", db.apikey)
if err != nil {
return fmt.Errorf("unable to create prober")
}

err = restoreDB.Probe(ctx)
if err != nil {
return err
}

db.log.Infow("meilisearch started after importing the dump, stopping it again")

return cmd.Process.Signal(syscall.SIGINT)
}, retry.Attempts(100), retry.Context(ctx))
if err != nil {
return handleFailedRecovery(err)
}

err = g.Wait()
if err != nil {
// will probably work better in meilisearch v1.4.0: https://github.com/meilisearch/meilisearch/commit/eff8570f591fe32a6106087807e3fe8c18e8e5e4
if strings.Contains(err.Error(), "interrupt") {
db.log.Infow("meilisearch terminated but reported an error which can be ignored", "error", err)
} else {
return handleFailedRecovery(err)
}
}

db.log.Info("successfully restored meilisearch database")

return nil
}
Loading