Skip to content

Commit

Permalink
Hello Redis Support (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
majst01 authored Nov 24, 2023
1 parent 7d94da0 commit 4dc86ef
Show file tree
Hide file tree
Showing 16 changed files with 1,280 additions and 172 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ start-etcd:
start-meilisearch:
$(MAKE) start DB=meilisearch

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

.PHONY: start
start: kind-cluster-create
kind --name backup-restore-sidecar load docker-image ghcr.io/metal-stack/backup-restore-sidecar:latest
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ 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 ||
| meilisearch | >= 1.2.0 | alpha ||
|-------------|--------------|:------:|:---------------:|
| postgres | >= 12-alpine | beta ||
| rethinkdb | >= 2.4.0 | beta ||
| ETCD | >= 3.5 | alpha ||
| meilisearch | >= 1.2.0 | alpha ||
| redis | >= 6.0 | alpha ||
| keydb | >= 6.0 | alpha ||

## Database Upgrades

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

import (
"context"
"fmt"
"os"
"path"
"time"

"github.com/redis/go-redis/v9"
"github.com/spf13/afero"

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

const (
redisDumpFile = "dump.rdb"
)

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

client *redis.Client
}

// New instantiates a new redis database
func New(log *zap.SugaredLogger, datadir string, addr string, password *string) (*Redis, error) {
if addr == "" {
return nil, fmt.Errorf("redis addr cannot be empty")
}

opts := &redis.Options{
Addr: addr,
}
if password != nil {
opts.Password = *password
}

client := redis.NewClient(opts)

return &Redis{
log: log,
datadir: datadir,
executor: utils.NewExecutor(log),
client: client,
}, nil
}

// Backup takes a dump of redis with the redis client.
func (db *Redis) 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 {
return fmt.Errorf("could not create backup directory: %w", err)
}

start := time.Now()
_, err := db.client.Save(ctx).Result()
if err != nil {
return fmt.Errorf("could not create a dump: %w", err)
}
resp, err := db.client.ConfigGet(ctx, "dir").Result()
if err != nil {
return fmt.Errorf("could not get config: %w", err)
}
dumpDir := resp["dir"]
dumpFile := path.Join(dumpDir, redisDumpFile)

db.log.Infow("dump created successfully", "file", dumpFile, "duration", time.Since(start).String())

// we need to do a copy here and cannot simply rename as the file system is
// mounted by two containers. the dump is created in the database container,
// the copy is done in the backup-restore-sidecar container. os.Rename would
// lead to an error.

err = utils.Copy(afero.NewOsFs(), dumpFile, path.Join(constants.BackupDir, redisDumpFile))
if err != nil {
return fmt.Errorf("unable to copy dumpfile to backupdir: %w", err)
}

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

db.log.Debugw("successfully took backup of redis")
return nil
}

// Check indicates whether a restore of the database is required or not.
func (db *Redis) 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 *Redis) Probe(ctx context.Context) error {
_, err := db.client.Ping(ctx).Result()
if err != nil {
return fmt.Errorf("connection error: %w", err)
}

return nil
}

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

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 := utils.Copy(afero.NewOsFs(), dump, path.Join(db.datadir, redisDumpFile))
if err != nil {
return fmt.Errorf("unable to recover %w", err)
}

db.log.Infow("successfully restored redis database", "duration", time.Since(start).String())

return nil
}

// Upgrade performs an upgrade of the database in case a newer version of the database is detected.
func (db *Redis) Upgrade(_ context.Context) error {
return nil
}
21 changes: 20 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/etcd"
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/meilisearch"
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/postgres"
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/redis"
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/rethinkdb"
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/initializer"
"github.com/metal-stack/backup-restore-sidecar/cmd/internal/metrics"
Expand Down Expand Up @@ -61,6 +62,9 @@ const (
meilisearchURLFlg = "meilisearch-url"
meilisearchAPIKeyFlg = "meilisearch-apikey"

redisAddrFlg = "redis-addr"
redisPasswordFlg = "redis-password"

rethinkDBPasswordFileFlg = "rethinkdb-passwordfile"
rethinkDBURLFlg = "rethinkdb-url"

Expand Down Expand Up @@ -270,7 +274,7 @@ func init() {
rootCmd.AddCommand(startCmd, waitCmd, restoreCmd, createBackupCmd)

rootCmd.PersistentFlags().StringP(logLevelFlg, "", "info", "sets the application log level")
rootCmd.PersistentFlags().StringP(databaseFlg, "", "", "the kind of the database [postgres|rethinkdb|etcd|meilisearch]")
rootCmd.PersistentFlags().StringP(databaseFlg, "", "", "the kind of the database [postgres|rethinkdb|etcd|meilisearch|redis|keydb]")
rootCmd.PersistentFlags().StringP(databaseDatadirFlg, "", "", "the directory where the database stores its data in")

err := viper.BindPFlags(rootCmd.PersistentFlags())
Expand Down Expand Up @@ -441,6 +445,21 @@ func initDatabase() error {
if err != nil {
return err
}
case "redis", "keydb":
var err error
var password string
if viper.IsSet(redisPasswordFlg) {
password = viper.GetString(redisPasswordFlg)
}
db, err = redis.New(
logger.Named("redis"),
datadir,
viper.GetString(redisAddrFlg),
&password,
)
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported database type: %s", dbString)
}
Expand Down
148 changes: 148 additions & 0 deletions deploy/keydb-local.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# THESE EXAMPLES ARE GENERATED!
# Use them as a template for your deployment, but do not commit manual changes to these files.
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
creationTimestamp: null
labels:
app: keydb
name: keydb
spec:
replicas: 1
selector:
matchLabels:
app: keydb
serviceName: keydb
template:
metadata:
creationTimestamp: null
labels:
app: keydb
spec:
containers:
- command:
- backup-restore-sidecar
- wait
image: eqalpha/keydb:alpine
livenessProbe:
exec:
command:
- keydb-cli
- ping
failureThreshold: 3
initialDelaySeconds: 15
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
name: keydb
ports:
- containerPort: 6379
name: client
protocol: TCP
readinessProbe:
exec:
command:
- keydb-cli
- ping
failureThreshold: 3
initialDelaySeconds: 15
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
resources: {}
volumeMounts:
- mountPath: /data
name: data
- mountPath: /usr/local/bin/backup-restore-sidecar
name: bin-provision
subPath: backup-restore-sidecar
- mountPath: /etc/backup-restore-sidecar
name: backup-restore-sidecar-config
- command:
- backup-restore-sidecar
- start
- --log-level=debug
image: eqalpha/keydb:alpine
name: backup-restore-sidecar
ports:
- containerPort: 8000
name: grpc
resources: {}
volumeMounts:
- mountPath: /backup
name: backup
- mountPath: /data
name: data
- mountPath: /etc/backup-restore-sidecar
name: backup-restore-sidecar-config
- mountPath: /usr/local/bin/backup-restore-sidecar
name: bin-provision
subPath: backup-restore-sidecar
initContainers:
- command:
- cp
- /backup-restore-sidecar
- /bin-provision
image: ghcr.io/metal-stack/backup-restore-sidecar:latest
imagePullPolicy: IfNotPresent
name: backup-restore-sidecar-provider
resources: {}
volumeMounts:
- mountPath: /bin-provision
name: bin-provision
volumes:
- name: data
persistentVolumeClaim:
claimName: data
- name: backup
persistentVolumeClaim:
claimName: backup
- configMap:
name: backup-restore-sidecar-config-keydb
name: backup-restore-sidecar-config
- emptyDir: {}
name: bin-provision
updateStrategy: {}
volumeClaimTemplates:
- metadata:
creationTimestamp: null
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
status: {}
- metadata:
creationTimestamp: null
name: backup
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
status: {}
status:
availableReplicas: 0
replicas: 0
---
apiVersion: v1
data:
config.yaml: |
---
bind-addr: 0.0.0.0
db: keydb
db-data-directory: /data/
backup-provider: local
backup-cron-schedule: "*/1 * * * *"
object-prefix: keydb-test
redis-addr: localhost:6379
post-exec-cmds:
- keydb-server
kind: ConfigMap
metadata:
creationTimestamp: null
name: backup-restore-sidecar-config-keydb
Loading

0 comments on commit 4dc86ef

Please sign in to comment.