From 7cd119bd03e457a15f61a080800cd7a606e14d66 Mon Sep 17 00:00:00 2001 From: Manfred Schuller Date: Wed, 18 Nov 2020 16:25:53 +0100 Subject: [PATCH 01/10] localfs first skeleton --- Makefile | 7 ++++- cmd/internal/database/localfs/localfs.go | 35 ++++++++++++++++++++++++ cmd/main.go | 10 ++++++- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 cmd/internal/database/localfs/localfs.go diff --git a/Makefile b/Makefile index e09f437..00d1cd6 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,12 @@ endif .PHONY: 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 + go build -ldflags "-X 'github.com/metal-stack/v.Version=$(VERSION)' \ + -X 'github.com/metal-stack/v.Revision=$(GITVERSION)' \ + -X 'github.com/metal-stack/v.GitSHA1=$(SHA)' \ + -X 'github.com/metal-stack/v.BuildDate=$(BUILDDATE)' \ + $(EXTLDFLAGS)" \ + -tags 'osusergo netgo static_build' -o bin/backup-restore-sidecar github.com/metal-stack/backup-restore-sidecar/cmd strip bin/backup-restore-sidecar .PHONY: test diff --git a/cmd/internal/database/localfs/localfs.go b/cmd/internal/database/localfs/localfs.go new file mode 100644 index 0000000..7ee4d22 --- /dev/null +++ b/cmd/internal/database/localfs/localfs.go @@ -0,0 +1,35 @@ +package localfs + +import "go.uber.org/zap" + +type LocalFS struct { + datadir string + log *zap.SugaredLogger +} + +func New(log *zap.SugaredLogger, datadir string) *LocalFS { + return &LocalFS{ + datadir: datadir, + log: log, + } +} + +func (l *LocalFS) Check() (bool, error) { + //ToDo: check if Datadir empty -> true + return true, nil +} + +func (l *LocalFS) Backup() error { + //ToDo: put Datadir into compressed archive + return nil +} + +func (l *LocalFS) Recover() error { + //ToDo: decompress archive into Datadir + return nil +} + +func (l *LocalFS) Probe() error { + //Nothing to do, not a real Database + return nil +} diff --git a/cmd/main.go b/cmd/main.go index a581268..008d778 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,6 +9,8 @@ import ( "os/signal" "strings" + "github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/localfs" + v1 "github.com/metal-stack/backup-restore-sidecar/api/v1" "github.com/metal-stack/backup-restore-sidecar/cmd/internal/backup" "github.com/metal-stack/backup-restore-sidecar/cmd/internal/backup/providers" @@ -273,7 +275,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|redis|keydb]") + rootCmd.PersistentFlags().StringP(databaseFlg, "", "", "the kind of the database [postgres|rethinkdb|etcd|meilisearch|redis|keydb|localfs]") rootCmd.PersistentFlags().StringP(databaseDatadirFlg, "", "", "the directory where the database stores its data in") err := viper.BindPFlags(rootCmd.PersistentFlags()) @@ -459,6 +461,12 @@ func initDatabase() error { if err != nil { return err } + case "localfs": + db = localfs.New( + logger.Named("localfs"), + datadir, + ) + default: return fmt.Errorf("unsupported database type: %s", dbString) } From 2d351c9d53fcacead9197bee0f489c765730bad4 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 7 Feb 2024 13:24:42 +0100 Subject: [PATCH 02/10] Adapt. --- cmd/internal/database/localfs/localfs.go | 22 +++++++++++++++------- cmd/main.go | 6 ++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cmd/internal/database/localfs/localfs.go b/cmd/internal/database/localfs/localfs.go index 7ee4d22..dcd5539 100644 --- a/cmd/internal/database/localfs/localfs.go +++ b/cmd/internal/database/localfs/localfs.go @@ -1,35 +1,43 @@ package localfs -import "go.uber.org/zap" +import ( + "context" + "log/slog" +) type LocalFS struct { datadir string - log *zap.SugaredLogger + log *slog.Logger } -func New(log *zap.SugaredLogger, datadir string) *LocalFS { +func New(log *slog.Logger, datadir string) *LocalFS { return &LocalFS{ datadir: datadir, log: log, } } -func (l *LocalFS) Check() (bool, error) { +func (l *LocalFS) Check(ctx context.Context) (bool, error) { //ToDo: check if Datadir empty -> true return true, nil } -func (l *LocalFS) Backup() error { +func (l *LocalFS) Backup(ctx context.Context) error { //ToDo: put Datadir into compressed archive return nil } -func (l *LocalFS) Recover() error { +func (l *LocalFS) Recover(ctx context.Context) error { //ToDo: decompress archive into Datadir return nil } -func (l *LocalFS) Probe() error { +func (l *LocalFS) Probe(ctx context.Context) error { //Nothing to do, not a real Database return nil } + +func (_ *LocalFS) Upgrade(ctx context.Context) error { + // Nothing to do here + return nil +} diff --git a/cmd/main.go b/cmd/main.go index 008d778..68ea451 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,8 +9,6 @@ import ( "os/signal" "strings" - "github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/localfs" - v1 "github.com/metal-stack/backup-restore-sidecar/api/v1" "github.com/metal-stack/backup-restore-sidecar/cmd/internal/backup" "github.com/metal-stack/backup-restore-sidecar/cmd/internal/backup/providers" @@ -20,6 +18,7 @@ import ( "github.com/metal-stack/backup-restore-sidecar/cmd/internal/compress" "github.com/metal-stack/backup-restore-sidecar/cmd/internal/database" "github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/etcd" + "github.com/metal-stack/backup-restore-sidecar/cmd/internal/database/localfs" "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" @@ -463,10 +462,9 @@ func initDatabase() error { } case "localfs": db = localfs.New( - logger.Named("localfs"), + logger.WithGroup("localfs"), datadir, ) - default: return fmt.Errorf("unsupported database type: %s", dbString) } From 5040368eff053e4f60b37621073e7a871b789fbf Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 7 Feb 2024 13:25:34 +0100 Subject: [PATCH 03/10] Revert. --- Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 00d1cd6..e09f437 100644 --- a/Makefile +++ b/Makefile @@ -24,12 +24,7 @@ endif .PHONY: build build: generate-examples go mod tidy - go build -ldflags "-X 'github.com/metal-stack/v.Version=$(VERSION)' \ - -X 'github.com/metal-stack/v.Revision=$(GITVERSION)' \ - -X 'github.com/metal-stack/v.GitSHA1=$(SHA)' \ - -X 'github.com/metal-stack/v.BuildDate=$(BUILDDATE)' \ - $(EXTLDFLAGS)" \ - -tags 'osusergo netgo static_build' -o bin/backup-restore-sidecar github.com/metal-stack/backup-restore-sidecar/cmd + 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 .PHONY: test From 3d32606f513ed6e43d4861025e64abce101c7471 Mon Sep 17 00:00:00 2001 From: qrnvttrl Date: Mon, 12 Feb 2024 09:04:58 +0100 Subject: [PATCH 04/10] Implemented localfs --- cmd/internal/database/localfs/localfs.go | 68 ++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/cmd/internal/database/localfs/localfs.go b/cmd/internal/database/localfs/localfs.go index dcd5539..256ac0c 100644 --- a/cmd/internal/database/localfs/localfs.go +++ b/cmd/internal/database/localfs/localfs.go @@ -2,12 +2,24 @@ package localfs import ( "context" + "fmt" "log/slog" + "os" + + "github.com/metal-stack/backup-restore-sidecar/cmd/internal/utils" + "github.com/metal-stack/backup-restore-sidecar/pkg/constants" + "github.com/spf13/afero" +) + +const ( + backupDest = constants.BackupDir + "/localfs" + restoreSrc = constants.RestoreDir + "/localfs" ) type LocalFS struct { - datadir string - log *slog.Logger + datadir string + fileNames []string + log *slog.Logger } func New(log *slog.Logger, datadir string) *LocalFS { @@ -17,18 +29,66 @@ func New(log *slog.Logger, datadir string) *LocalFS { } } +// Check if Datadir contains the specified filenames func (l *LocalFS) Check(ctx context.Context) (bool, error) { - //ToDo: check if Datadir empty -> true - return true, nil + dirEntrys, err := os.ReadDir(l.datadir) + + if err != nil { + return false, err + } + + result := true + + for i := 0; i < len(l.fileNames) && result; i++ { + found := false + + for j := 0; j < len(dirEntrys) && !found; j++ { + curEntry, err := dirEntrys[j].Info() + + if err != nil { + return false, err + } + + found = l.fileNames[i] == curEntry.Name() + } + + result = found + } + + return result, nil } +// put Datadir into constants.Backupdir directory func (l *LocalFS) Backup(ctx context.Context) error { //ToDo: put Datadir into compressed archive + + 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) + } + + if err := utils.Copy(afero.NewOsFs(), l.datadir, backupDest); err != nil { + return fmt.Errorf("could not copy contents: %w", err) + } + + l.log.Debug("Sucessfully took backup of localfs") return nil } func (l *LocalFS) Recover(ctx context.Context) error { //ToDo: decompress archive into Datadir + if err := utils.RemoveContents(l.datadir); err != nil { + return fmt.Errorf("Could not cleanup Datadir: %w", err) + } + + if err := utils.Copy(afero.NewOsFs(), restoreSrc, l.datadir); err != nil { + return fmt.Errorf("could not copy contents: %w", err) + } + + l.log.Debug("Successfully restored localfs") return nil } From d7e98b21ae4619de825264182780019b95109b8e Mon Sep 17 00:00:00 2001 From: qrnvttrl Date: Wed, 14 Feb 2024 14:31:58 +0100 Subject: [PATCH 05/10] added Integration test --- deploy/localfs-local.yaml | 124 +++++++++++++ go.mod | 1 + go.sum | 5 + integration/localfs_test.go | 34 ++++ integration/main_test.go | 43 ++++- pkg/generate/examples/dump.go | 5 + pkg/generate/examples/examples/localfs.go | 216 ++++++++++++++++++++++ 7 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 deploy/localfs-local.yaml create mode 100644 integration/localfs_test.go create mode 100644 pkg/generate/examples/examples/localfs.go diff --git a/deploy/localfs-local.yaml b/deploy/localfs-local.yaml new file mode 100644 index 0000000..103395e --- /dev/null +++ b/deploy/localfs-local.yaml @@ -0,0 +1,124 @@ +# 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: localfs + name: localfs +spec: + replicas: 1 + selector: + matchLabels: + app: localfs + serviceName: localfs + template: + metadata: + creationTimestamp: null + labels: + app: localfs + spec: + containers: + - command: + - backup-restore-sidecar + - wait + image: alpine:3.19 + name: localfs + 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: alpine:3.19 + 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-localfs + 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: localfs + db-data-directory: /data/ + backup-provider: local + backup-cron-schedule: "*/1 * * * *" + object-prefix: localfs-test + redis-addr: localhost:6379 + post-exec-cmds: + - tail -f /etc/hosts +kind: ConfigMap +metadata: + creationTimestamp: null + name: backup-restore-sidecar-config-localfs diff --git a/go.mod b/go.mod index 52ce72d..9b13063 100644 --- a/go.mod +++ b/go.mod @@ -97,6 +97,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect diff --git a/go.sum b/go.sum index 0b3c0f9..5e84959 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/avast/retry-go/v4 v4.5.1 h1:AxIx0HGi4VZ3I02jr78j5lZ3M6x1E0Ivxa6b0pUUh7o= github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc= github.com/aws/aws-sdk-go v1.45.7 h1:k4QsvWZhm8409TYeRuTV1P6+j3lLKoe+giFA/j3VAps= @@ -179,6 +181,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= @@ -246,6 +249,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= diff --git a/integration/localfs_test.go b/integration/localfs_test.go new file mode 100644 index 0000000..fcac7f5 --- /dev/null +++ b/integration/localfs_test.go @@ -0,0 +1,34 @@ +//go:build integration + +package integration_test + +import ( + "context" + "testing" + + "github.com/metal-stack/backup-restore-sidecar/pkg/generate/examples/examples" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Localfs_Restore(t *testing.T) { + restoreFlow(t, &flowSpec{ + databaseType: examples.Localfs, + sts: examples.LocalfsSts, + backingResources: examples.LocalfsBackingResources, + addTestData: addLocalfsTestData, + verifyTestData: verifyLocalfsTestData, + }) +} + +func addLocalfsTestData(t *testing.T, ctx context.Context) { + _, _, err := execCommand(ctx, "backup-restore-sidecar", []string{"sh", "-c", "echo 'I am precious' > /data/test.txt"}) + require.NoError(t, err) +} + +func verifyLocalfsTestData(t *testing.T, ctx context.Context) { + resp, _, err := execCommand(ctx, "backup-restore-sidecar", []string{"cat", "/data/test.txt"}) + require.NoError(t, err) + + assert.Equal(t, "I am precious", resp) +} diff --git a/integration/main_test.go b/integration/main_test.go index ad0bc89..0f7c774 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -3,6 +3,7 @@ package integration_test import ( + "bytes" "context" "errors" "fmt" @@ -23,7 +24,10 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" ) type flowSpec struct { @@ -45,6 +49,7 @@ type upgradeFlowSpec struct { var ( restConfig *rest.Config c client.Client + pod *corev1.Pod ) func TestMain(m *testing.M) { @@ -327,7 +332,7 @@ func newKubernetesClient() (client.Client, error) { func waitForPodRunning(ctx context.Context, name, namespace string) error { return retry.Do(func() error { - pod := &corev1.Pod{ + pod = &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -396,3 +401,39 @@ func waitUntilNotFound(ctx context.Context, obj client.Object) error { return fmt.Errorf("resource is still running: %s", obj.GetName()) }, retry.Context(ctx), retry.Attempts(0), retry.MaxDelay(2*time.Second)) } + +func execCommand(ctx context.Context, containerName string, cmd []string) (string, string, error) { + var stdout, stderr bytes.Buffer + client, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return "", "", err + } + + req := client.CoreV1().RESTClient().Post().Resource("pods").Name(pod.Name).Namespace(pod.Namespace).SubResource("exec") + option := &corev1.PodExecOptions{ + Command: cmd, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: true, + Container: containerName, + } + + req.VersionedParams( + option, + scheme.ParameterCodec, + ) + exec, err := remotecommand.NewSPDYExecutor(restConfig, "POST", req.URL()) + if err != nil { + return "", "", err + } + err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{ + Stdin: nil, + Stdout: &stdout, + Stderr: &stderr, + }) + if err != nil { + return "", "", err + } + return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), nil +} diff --git a/pkg/generate/examples/dump.go b/pkg/generate/examples/dump.go index c51f708..6bb3f00 100644 --- a/pkg/generate/examples/dump.go +++ b/pkg/generate/examples/dump.go @@ -49,6 +49,11 @@ func main() { sts: examples.KeyDBSts, backing: examples.KeyDBBackingResources, }, + { + db: examples.Localfs, + sts: examples.LocalfsSts, + backing: examples.LocalfsBackingResources, + }, } { err := dumpToExamples(localExample.db+"-local.yaml", append([]client.Object{localExample.sts("default")}, localExample.backing("default")...)...) if err != nil { diff --git a/pkg/generate/examples/examples/localfs.go b/pkg/generate/examples/examples/localfs.go new file mode 100644 index 0000000..31c48cd --- /dev/null +++ b/pkg/generate/examples/examples/localfs.go @@ -0,0 +1,216 @@ +package examples + +import ( + "github.com/metal-stack/backup-restore-sidecar/pkg/constants" + "github.com/metal-stack/metal-lib/pkg/pointer" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + Localfs = "localfs" + LocalfsContainerImage = "alpine:3.19" +) + +func LocalfsSts(namespace string) *appsv1.StatefulSet { + return &appsv1.StatefulSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "StatefulSet", + APIVersion: appsv1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "localfs", + Namespace: namespace, + Labels: map[string]string{ + "app": "localfs", + }, + }, + Spec: appsv1.StatefulSetSpec{ + ServiceName: "localfs", + Replicas: pointer.Pointer(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "localfs", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "localfs", + }, + }, + Spec: corev1.PodSpec{ + HostNetwork: true, + Containers: []corev1.Container{ + { + Name: "localfs", + Image: LocalfsContainerImage, + Command: []string{"backup-restore-sidecar", "wait"}, + + VolumeMounts: []corev1.VolumeMount{ + { + Name: "data", + MountPath: "/data", + }, + { + Name: "bin-provision", + SubPath: "backup-restore-sidecar", + MountPath: "/usr/local/bin/backup-restore-sidecar", + }, + { + Name: "backup-restore-sidecar-config", + MountPath: "/etc/backup-restore-sidecar", + }, + }, + }, + { + Name: "backup-restore-sidecar", + Image: LocalfsContainerImage, + Command: []string{"backup-restore-sidecar", "start", "--log-level=debug"}, + Ports: []corev1.ContainerPort{ + { + Name: "grpc", + ContainerPort: 8000, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "backup", + MountPath: constants.SidecarBaseDir, + }, + { + Name: "data", + MountPath: "/data", + }, + { + Name: "backup-restore-sidecar-config", + MountPath: "/etc/backup-restore-sidecar", + }, + { + Name: "bin-provision", + SubPath: "backup-restore-sidecar", + MountPath: "/usr/local/bin/backup-restore-sidecar", + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "backup-restore-sidecar-provider", + Image: backupRestoreSidecarContainerImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "cp", + "/backup-restore-sidecar", + "/bin-provision", + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "bin-provision", + MountPath: "/bin-provision", + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "data", + }, + }, + }, + { + Name: "backup", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "backup", + }, + }, + }, + { + Name: "backup-restore-sidecar-config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "backup-restore-sidecar-config-localfs", + }, + }, + }, + }, + { + Name: "bin-provision", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "data", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "backup", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + }, + }, + }, + } +} + +func LocalfsBackingResources(namespace string) []client.Object { + return []client.Object{ + &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: corev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "backup-restore-sidecar-config-localfs", + Namespace: namespace, + }, + Data: map[string]string{ + "config.yaml": `--- +bind-addr: 0.0.0.0 +db: localfs +db-data-directory: /data/ +backup-provider: local +backup-cron-schedule: "*/1 * * * *" +object-prefix: localfs-test +redis-addr: localhost:6379 +post-exec-cmds: +- tail -f /etc/hosts +`, + }, + }, + } +} From d2e161d5237abbc57d915f5a3ad33b312e508ff9 Mon Sep 17 00:00:00 2001 From: qrnvttrl Date: Wed, 14 Feb 2024 14:32:28 +0100 Subject: [PATCH 06/10] fixed error + start localfs in makefile --- Makefile | 4 +++ cmd/internal/database/localfs/localfs.go | 39 ++++++------------------ cmd/internal/utils/files.go | 21 +++++++++++++ 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/Makefile b/Makefile index e09f437..6092a81 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,10 @@ start-meilisearch: start-redis: $(MAKE) start DB=redis +.PHONY: start-localfs +start-localfs: + $(MAKE) start DB=localfs + .PHONY: start start: kind-cluster-create kind --name backup-restore-sidecar load docker-image ghcr.io/metal-stack/backup-restore-sidecar:latest diff --git a/cmd/internal/database/localfs/localfs.go b/cmd/internal/database/localfs/localfs.go index 256ac0c..7a861d3 100644 --- a/cmd/internal/database/localfs/localfs.go +++ b/cmd/internal/database/localfs/localfs.go @@ -11,11 +11,6 @@ import ( "github.com/spf13/afero" ) -const ( - backupDest = constants.BackupDir + "/localfs" - restoreSrc = constants.RestoreDir + "/localfs" -) - type LocalFS struct { datadir string fileNames []string @@ -29,36 +24,21 @@ func New(log *slog.Logger, datadir string) *LocalFS { } } -// Check if Datadir contains the specified filenames +// Check if Datadir is empty func (l *LocalFS) Check(ctx context.Context) (bool, error) { - dirEntrys, err := os.ReadDir(l.datadir) - + empty, err := utils.IsEmpty(l.datadir) if err != nil { return false, err } - - result := true - - for i := 0; i < len(l.fileNames) && result; i++ { - found := false - - for j := 0; j < len(dirEntrys) && !found; j++ { - curEntry, err := dirEntrys[j].Info() - - if err != nil { - return false, err - } - - found = l.fileNames[i] == curEntry.Name() - } - - result = found + if empty { + l.log.Info("data directory is empty") + return true, err } - return result, nil + return false, nil } -// put Datadir into constants.Backupdir directory +// put Datadir into constants.BackupDir directory func (l *LocalFS) Backup(ctx context.Context) error { //ToDo: put Datadir into compressed archive @@ -70,7 +50,7 @@ func (l *LocalFS) Backup(ctx context.Context) error { return fmt.Errorf("could not create backup directory: %w", err) } - if err := utils.Copy(afero.NewOsFs(), l.datadir, backupDest); err != nil { + if err := utils.CopyDirectory(afero.NewOsFs(), l.datadir, constants.BackupDir); err != nil { return fmt.Errorf("could not copy contents: %w", err) } @@ -78,13 +58,14 @@ func (l *LocalFS) Backup(ctx context.Context) error { return nil } +// get data from constants.RestoreDir func (l *LocalFS) Recover(ctx context.Context) error { //ToDo: decompress archive into Datadir if err := utils.RemoveContents(l.datadir); err != nil { return fmt.Errorf("Could not cleanup Datadir: %w", err) } - if err := utils.Copy(afero.NewOsFs(), restoreSrc, l.datadir); err != nil { + if err := utils.CopyDirectory(afero.NewOsFs(), constants.RestoreDir, l.datadir); err != nil { return fmt.Errorf("could not copy contents: %w", err) } diff --git a/cmd/internal/utils/files.go b/cmd/internal/utils/files.go index 102680e..a5b5098 100644 --- a/cmd/internal/utils/files.go +++ b/cmd/internal/utils/files.go @@ -80,3 +80,24 @@ func IsCommandPresent(command string) bool { return true } + +func CopyDirectory(osfs afero.Fs, dir string, dest string) error { + + contents, err := os.ReadDir(dir) + if err != nil { + return err + } + + for _, content := range contents { + if content.Type().IsDir() { + err = CopyDirectory(osfs, dir+"/"+content.Name(), dest+"/"+content.Name()) + } else { + err = Copy(osfs, dir+"/"+content.Name(), dest+"/"+content.Name()) + } + if err != nil { + return err + } + } + + return nil +} From 737d74f0e6f998b3de9df5e50343100723c5ddc0 Mon Sep 17 00:00:00 2001 From: qrnvttrl Date: Wed, 14 Feb 2024 14:49:14 +0100 Subject: [PATCH 07/10] fixed error --- cmd/internal/database/localfs/localfs.go | 5 ++--- integration/main_test.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/internal/database/localfs/localfs.go b/cmd/internal/database/localfs/localfs.go index 7a861d3..9a52635 100644 --- a/cmd/internal/database/localfs/localfs.go +++ b/cmd/internal/database/localfs/localfs.go @@ -12,9 +12,8 @@ import ( ) type LocalFS struct { - datadir string - fileNames []string - log *slog.Logger + datadir string + log *slog.Logger } func New(log *slog.Logger, datadir string) *LocalFS { diff --git a/integration/main_test.go b/integration/main_test.go index 0f7c774..2b376a3 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -427,7 +427,7 @@ func execCommand(ctx context.Context, containerName string, cmd []string) (strin if err != nil { return "", "", err } - err = exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{ + err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ Stdin: nil, Stdout: &stdout, Stderr: &stderr, From 1698b3b20d6c8ade40abcc190266c8e5188407b9 Mon Sep 17 00:00:00 2001 From: qrnvttrl Date: Wed, 14 Feb 2024 15:21:49 +0100 Subject: [PATCH 08/10] fixed error --- integration/localfs_test.go | 4 ++-- integration/main_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration/localfs_test.go b/integration/localfs_test.go index fcac7f5..a97e79d 100644 --- a/integration/localfs_test.go +++ b/integration/localfs_test.go @@ -22,12 +22,12 @@ func Test_Localfs_Restore(t *testing.T) { } func addLocalfsTestData(t *testing.T, ctx context.Context) { - _, _, err := execCommand(ctx, "backup-restore-sidecar", []string{"sh", "-c", "echo 'I am precious' > /data/test.txt"}) + _, err := execCommand(ctx, "backup-restore-sidecar", []string{"sh", "-c", "echo 'I am precious' > /data/test.txt"}) require.NoError(t, err) } func verifyLocalfsTestData(t *testing.T, ctx context.Context) { - resp, _, err := execCommand(ctx, "backup-restore-sidecar", []string{"cat", "/data/test.txt"}) + resp, err := execCommand(ctx, "backup-restore-sidecar", []string{"cat", "/data/test.txt"}) require.NoError(t, err) assert.Equal(t, "I am precious", resp) diff --git a/integration/main_test.go b/integration/main_test.go index 2b376a3..efbba54 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -402,11 +402,11 @@ func waitUntilNotFound(ctx context.Context, obj client.Object) error { }, retry.Context(ctx), retry.Attempts(0), retry.MaxDelay(2*time.Second)) } -func execCommand(ctx context.Context, containerName string, cmd []string) (string, string, error) { +func execCommand(ctx context.Context, containerName string, cmd []string) (string, error) { var stdout, stderr bytes.Buffer client, err := kubernetes.NewForConfig(restConfig) if err != nil { - return "", "", err + return "", err } req := client.CoreV1().RESTClient().Post().Resource("pods").Name(pod.Name).Namespace(pod.Namespace).SubResource("exec") @@ -425,15 +425,15 @@ func execCommand(ctx context.Context, containerName string, cmd []string) (strin ) exec, err := remotecommand.NewSPDYExecutor(restConfig, "POST", req.URL()) if err != nil { - return "", "", err + return "", err } - err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + err = exec.StreamWithContext(context.WithoutCancel(ctx), remotecommand.StreamOptions{ Stdin: nil, Stdout: &stdout, Stderr: &stderr, }) if err != nil { - return "", "", err + return "", err } - return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), nil + return strings.TrimSpace(stdout.String()), nil } From 4d72f39deb634ffc9974b171a134e663309e9623 Mon Sep 17 00:00:00 2001 From: qrnvttrl Date: Thu, 15 Feb 2024 10:49:34 +0100 Subject: [PATCH 09/10] changes after PR-comments --- README.md | 1 + cmd/internal/database/localfs/localfs.go | 8 ++--- cmd/internal/utils/files.go | 44 ++++++++++++++++-------- integration/localfs_test.go | 6 ++-- integration/main_test.go | 9 +++-- 5 files changed, 40 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index cdc6007..4d1741d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Probably, it does not make sense to use this project with large databases. Howev | meilisearch | >= 1.2.0 | alpha | ✅ | | redis | >= 6.0 | alpha | ❌ | | keydb | >= 6.0 | alpha | ❌ | +| localfs | alpine | alpha | ❌ | Postgres also supports updates when using the TimescaleDB extension. Please consider the integration test for supported upgrade paths. diff --git a/cmd/internal/database/localfs/localfs.go b/cmd/internal/database/localfs/localfs.go index 9a52635..b7c80a9 100644 --- a/cmd/internal/database/localfs/localfs.go +++ b/cmd/internal/database/localfs/localfs.go @@ -8,7 +8,6 @@ import ( "github.com/metal-stack/backup-restore-sidecar/cmd/internal/utils" "github.com/metal-stack/backup-restore-sidecar/pkg/constants" - "github.com/spf13/afero" ) type LocalFS struct { @@ -39,8 +38,6 @@ func (l *LocalFS) Check(ctx context.Context) (bool, error) { // put Datadir into constants.BackupDir directory func (l *LocalFS) Backup(ctx context.Context) error { - //ToDo: put Datadir into compressed archive - if err := os.RemoveAll(constants.BackupDir); err != nil { return fmt.Errorf("could not clean backup directory: %w", err) } @@ -49,7 +46,7 @@ func (l *LocalFS) Backup(ctx context.Context) error { return fmt.Errorf("could not create backup directory: %w", err) } - if err := utils.CopyDirectory(afero.NewOsFs(), l.datadir, constants.BackupDir); err != nil { + if err := utils.CopyFS(constants.BackupDir, os.DirFS(l.datadir)); err != nil { return fmt.Errorf("could not copy contents: %w", err) } @@ -59,12 +56,11 @@ func (l *LocalFS) Backup(ctx context.Context) error { // get data from constants.RestoreDir func (l *LocalFS) Recover(ctx context.Context) error { - //ToDo: decompress archive into Datadir if err := utils.RemoveContents(l.datadir); err != nil { return fmt.Errorf("Could not cleanup Datadir: %w", err) } - if err := utils.CopyDirectory(afero.NewOsFs(), constants.RestoreDir, l.datadir); err != nil { + if err := utils.CopyFS(l.datadir, os.DirFS(constants.RestoreDir)); err != nil { return fmt.Errorf("could not copy contents: %w", err) } diff --git a/cmd/internal/utils/files.go b/cmd/internal/utils/files.go index a5b5098..4784e44 100644 --- a/cmd/internal/utils/files.go +++ b/cmd/internal/utils/files.go @@ -2,6 +2,7 @@ package utils import ( "errors" + "fmt" "io" "io/fs" "os" @@ -81,23 +82,36 @@ func IsCommandPresent(command string) bool { return true } -func CopyDirectory(osfs afero.Fs, dir string, dest string) error { - - contents, err := os.ReadDir(dir) - if err != nil { - return err - } - - for _, content := range contents { - if content.Type().IsDir() { - err = CopyDirectory(osfs, dir+"/"+content.Name(), dest+"/"+content.Name()) - } else { - err = Copy(osfs, dir+"/"+content.Name(), dest+"/"+content.Name()) +// TODO: replace once go-1.23 is released +func CopyFS(dir string, fsys fs.FS) error { + return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, error error) error { + targ := filepath.Join(dir, filepath.FromSlash(path)) + if d.IsDir() { + if err := os.MkdirAll(targ, 0777); err != nil { + return err + } + return nil } + r, err := fsys.Open(path) if err != nil { return err } - } - - return nil + defer r.Close() + info, err := r.Stat() + if err != nil { + return err + } + w, err := os.OpenFile(targ, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666|info.Mode()&0777) + if err != nil { + return err + } + if _, err := io.Copy(w, r); err != nil { + w.Close() + return fmt.Errorf("copying %s: %w", path, err) + } + if err := w.Close(); err != nil { + return err + } + return nil + }) } diff --git a/integration/localfs_test.go b/integration/localfs_test.go index a97e79d..15fd0b2 100644 --- a/integration/localfs_test.go +++ b/integration/localfs_test.go @@ -22,12 +22,14 @@ func Test_Localfs_Restore(t *testing.T) { } func addLocalfsTestData(t *testing.T, ctx context.Context) { - _, err := execCommand(ctx, "backup-restore-sidecar", []string{"sh", "-c", "echo 'I am precious' > /data/test.txt"}) + namespace := namespaceName(t) + _, err := execCommand(ctx, "localfs-0", namespace, "backup-restore-sidecar", []string{"sh", "-c", "echo 'I am precious' > /data/test.txt"}) require.NoError(t, err) } func verifyLocalfsTestData(t *testing.T, ctx context.Context) { - resp, err := execCommand(ctx, "backup-restore-sidecar", []string{"cat", "/data/test.txt"}) + namespace := namespaceName(t) + resp, err := execCommand(ctx, "localfs-0", namespace, "backup-restore-sidecar", []string{"cat", "/data/test.txt"}) require.NoError(t, err) assert.Equal(t, "I am precious", resp) diff --git a/integration/main_test.go b/integration/main_test.go index efbba54..9d3485b 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -49,7 +49,6 @@ type upgradeFlowSpec struct { var ( restConfig *rest.Config c client.Client - pod *corev1.Pod ) func TestMain(m *testing.M) { @@ -332,7 +331,7 @@ func newKubernetesClient() (client.Client, error) { func waitForPodRunning(ctx context.Context, name, namespace string) error { return retry.Do(func() error { - pod = &corev1.Pod{ + pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, @@ -402,14 +401,14 @@ func waitUntilNotFound(ctx context.Context, obj client.Object) error { }, retry.Context(ctx), retry.Attempts(0), retry.MaxDelay(2*time.Second)) } -func execCommand(ctx context.Context, containerName string, cmd []string) (string, error) { +func execCommand(ctx context.Context, podName string, namespace string, containerName string, cmd []string) (string, error) { var stdout, stderr bytes.Buffer client, err := kubernetes.NewForConfig(restConfig) if err != nil { return "", err } - req := client.CoreV1().RESTClient().Post().Resource("pods").Name(pod.Name).Namespace(pod.Namespace).SubResource("exec") + req := client.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace(namespace).SubResource("exec") option := &corev1.PodExecOptions{ Command: cmd, Stdin: false, @@ -427,7 +426,7 @@ func execCommand(ctx context.Context, containerName string, cmd []string) (strin if err != nil { return "", err } - err = exec.StreamWithContext(context.WithoutCancel(ctx), remotecommand.StreamOptions{ + err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ Stdin: nil, Stdout: &stdout, Stderr: &stderr, From 20c6fe2a71f41f56c9da2405989a5d2bd0703878 Mon Sep 17 00:00:00 2001 From: qrnvttrl Date: Thu, 15 Feb 2024 15:14:06 +0100 Subject: [PATCH 10/10] fixed readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d1741d..44f450a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Probably, it does not make sense to use this project with large databases. Howev | meilisearch | >= 1.2.0 | alpha | ✅ | | redis | >= 6.0 | alpha | ❌ | | keydb | >= 6.0 | alpha | ❌ | -| localfs | alpine | alpha | ❌ | +| localfs | | alpha | ❌ | Postgres also supports updates when using the TimescaleDB extension. Please consider the integration test for supported upgrade paths.