From 73773dffc5ab9464af92b8c39232c3f4b4bb5a23 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 25 Aug 2023 16:56:03 +0200 Subject: [PATCH] Add manual backup possibility. --- .github/workflows/docker.yaml | 5 +- api/v1/database.pb.go | 147 ++++++++++++++++++ api/v1/database_grpc.pb.go | 107 +++++++++++++ cmd/internal/backup/backup.go | 84 +++++----- cmd/internal/initializer/initializer.go | 10 +- cmd/internal/initializer/service.go | 19 +++ cmd/main.go | 28 +++- deploy/rethinkdb-local.yaml | 198 ++++++++++++++---------- deploy/rethinkdb-test.yaml | 177 --------------------- integration/main_test.go | 8 +- integration/rethinkdb_test.go | 30 +++- pkg/client/client.go | 5 + proto/v1/database.proto | 11 ++ 13 files changed, 514 insertions(+), 315 deletions(-) create mode 100644 api/v1/database.pb.go create mode 100644 api/v1/database_grpc.pb.go delete mode 100644 deploy/rethinkdb-test.yaml create mode 100644 proto/v1/database.proto diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index b2ff6a2..6b28d03 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -72,10 +72,11 @@ jobs: go-version: '1.21.x' - name: Create k8s Kind Cluster - uses: helm/kind-action@v1.5.0 + uses: helm/kind-action@v1.8.0 with: install_only: true - name: Test run: | - make test + make kind-cluster-create + make test-integration diff --git a/api/v1/database.pb.go b/api/v1/database.pb.go new file mode 100644 index 0000000..fa69b22 --- /dev/null +++ b/api/v1/database.pb.go @@ -0,0 +1,147 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: v1/database.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CreateBackupResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CreateBackupResponse) Reset() { + *x = CreateBackupResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_v1_database_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateBackupResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateBackupResponse) ProtoMessage() {} + +func (x *CreateBackupResponse) ProtoReflect() protoreflect.Message { + mi := &file_v1_database_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateBackupResponse.ProtoReflect.Descriptor instead. +func (*CreateBackupResponse) Descriptor() ([]byte, []int) { + return file_v1_database_proto_rawDescGZIP(), []int{0} +} + +var File_v1_database_proto protoreflect.FileDescriptor + +var file_v1_database_proto_rawDesc = []byte{ + 0x0a, 0x11, 0x76, 0x31, 0x2f, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x76, 0x31, 0x1a, 0x0f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x16, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x32, 0x46, 0x0a, 0x0f, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x33, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x12, 0x09, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x18, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x69, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x2e, + 0x76, 0x31, 0x42, 0x0d, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6d, 0x65, 0x74, 0x61, 0x6c, 0x2d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x64, 0x72, 0x6f, 0x70, + 0x74, 0x61, 0x69, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0xa2, 0x02, 0x03, + 0x56, 0x58, 0x58, 0xaa, 0x02, 0x02, 0x56, 0x31, 0xca, 0x02, 0x02, 0x56, 0x31, 0xe2, 0x02, 0x0e, + 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x02, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_v1_database_proto_rawDescOnce sync.Once + file_v1_database_proto_rawDescData = file_v1_database_proto_rawDesc +) + +func file_v1_database_proto_rawDescGZIP() []byte { + file_v1_database_proto_rawDescOnce.Do(func() { + file_v1_database_proto_rawDescData = protoimpl.X.CompressGZIP(file_v1_database_proto_rawDescData) + }) + return file_v1_database_proto_rawDescData +} + +var file_v1_database_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_v1_database_proto_goTypes = []interface{}{ + (*CreateBackupResponse)(nil), // 0: v1.CreateBackupResponse + (*Empty)(nil), // 1: v1.Empty +} +var file_v1_database_proto_depIdxs = []int32{ + 1, // 0: v1.DatabaseService.CreateBackup:input_type -> v1.Empty + 0, // 1: v1.DatabaseService.CreateBackup:output_type -> v1.CreateBackupResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_v1_database_proto_init() } +func file_v1_database_proto_init() { + if File_v1_database_proto != nil { + return + } + file_v1_common_proto_init() + if !protoimpl.UnsafeEnabled { + file_v1_database_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateBackupResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_v1_database_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_v1_database_proto_goTypes, + DependencyIndexes: file_v1_database_proto_depIdxs, + MessageInfos: file_v1_database_proto_msgTypes, + }.Build() + File_v1_database_proto = out.File + file_v1_database_proto_rawDesc = nil + file_v1_database_proto_goTypes = nil + file_v1_database_proto_depIdxs = nil +} diff --git a/api/v1/database_grpc.pb.go b/api/v1/database_grpc.pb.go new file mode 100644 index 0000000..d066d40 --- /dev/null +++ b/api/v1/database_grpc.pb.go @@ -0,0 +1,107 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc (unknown) +// source: v1/database.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + DatabaseService_CreateBackup_FullMethodName = "/v1.DatabaseService/CreateBackup" +) + +// DatabaseServiceClient is the client API for DatabaseService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type DatabaseServiceClient interface { + CreateBackup(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CreateBackupResponse, error) +} + +type databaseServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDatabaseServiceClient(cc grpc.ClientConnInterface) DatabaseServiceClient { + return &databaseServiceClient{cc} +} + +func (c *databaseServiceClient) CreateBackup(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*CreateBackupResponse, error) { + out := new(CreateBackupResponse) + err := c.cc.Invoke(ctx, DatabaseService_CreateBackup_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DatabaseServiceServer is the server API for DatabaseService service. +// All implementations should embed UnimplementedDatabaseServiceServer +// for forward compatibility +type DatabaseServiceServer interface { + CreateBackup(context.Context, *Empty) (*CreateBackupResponse, error) +} + +// UnimplementedDatabaseServiceServer should be embedded to have forward compatible implementations. +type UnimplementedDatabaseServiceServer struct { +} + +func (UnimplementedDatabaseServiceServer) CreateBackup(context.Context, *Empty) (*CreateBackupResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateBackup not implemented") +} + +// UnsafeDatabaseServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DatabaseServiceServer will +// result in compilation errors. +type UnsafeDatabaseServiceServer interface { + mustEmbedUnimplementedDatabaseServiceServer() +} + +func RegisterDatabaseServiceServer(s grpc.ServiceRegistrar, srv DatabaseServiceServer) { + s.RegisterService(&DatabaseService_ServiceDesc, srv) +} + +func _DatabaseService_CreateBackup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DatabaseServiceServer).CreateBackup(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DatabaseService_CreateBackup_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DatabaseServiceServer).CreateBackup(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// DatabaseService_ServiceDesc is the grpc.ServiceDesc for DatabaseService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DatabaseService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "v1.DatabaseService", + HandlerType: (*DatabaseServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateBackup", + Handler: _DatabaseService_CreateBackup_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "v1/database.proto", +} diff --git a/cmd/internal/backup/backup.go b/cmd/internal/backup/backup.go index ffe9e21..13a8db7 100644 --- a/cmd/internal/backup/backup.go +++ b/cmd/internal/backup/backup.go @@ -2,6 +2,7 @@ package backup import ( "context" + "fmt" "os" "path" @@ -21,46 +22,11 @@ func Start(ctx context.Context, log *zap.SugaredLogger, backupSchedule string, d c := cron.New() id, err := c.AddFunc(backupSchedule, func() { - err := db.Backup() + err := CreateBackup(log, db, bp, metrics, comp) if err != nil { - metrics.CountError("create") - log.Errorw("database backup failed", "error", err) - return + log.Errorw("error creating backup", "error", err) } - log.Infow("successfully backed up database") - backupArchiveName := bp.GetNextBackupName() - - backupFilePath := path.Join(constants.BackupDir, backupArchiveName) - if err := os.RemoveAll(backupFilePath + comp.Extension()); err != nil { - metrics.CountError("delete_prior") - log.Errorw("could not delete priorly uploaded backup", "error", err) - return - } - - filename, err := comp.Compress(backupFilePath) - if err != nil { - metrics.CountError("compress") - log.Errorw("unable to compress backup", "error", err) - return - } - log.Info("compressed backup") - - err = bp.UploadBackup(filename) - if err != nil { - metrics.CountError("upload") - log.Errorw("error uploading backup", "error", err) - return - } - log.Info("uploaded backup to backup provider bucket") - metrics.CountBackup(filename) - err = bp.CleanupBackups() - if err != nil { - metrics.CountError("cleanup") - log.Errorw("cleaning up backups failed", "error", err) - } else { - log.Infow("cleaned up backups") - } for _, e := range c.Entries() { log.Infow("scheduling next backup", "at", e.Next.String()) } @@ -75,3 +41,47 @@ func Start(ctx context.Context, log *zap.SugaredLogger, backupSchedule string, d c.Stop() return nil } + +func CreateBackup(log *zap.SugaredLogger, db database.DatabaseProber, bp backuproviders.BackupProvider, metrics *metrics.Metrics, comp *compress.Compressor) error { + err := db.Backup() + if err != nil { + metrics.CountError("create") + return fmt.Errorf("database backup failed: %w", err) + } + + log.Infow("successfully backed up database") + + backupArchiveName := bp.GetNextBackupName() + + backupFilePath := path.Join(constants.BackupDir, backupArchiveName) + if err := os.RemoveAll(backupFilePath + comp.Extension()); err != nil { + metrics.CountError("delete_prior") + return fmt.Errorf("could not delete priorly uploaded backup: %w", err) + } + + filename, err := comp.Compress(backupFilePath) + if err != nil { + metrics.CountError("compress") + return fmt.Errorf("unable to compress backup: %w", err) + } + log.Info("compressed backup") + + err = bp.UploadBackup(filename) + if err != nil { + metrics.CountError("upload") + return fmt.Errorf("error uploading backup: %w", err) + } + log.Info("uploaded backup to backup provider bucket") + + metrics.CountBackup(filename) + + err = bp.CleanupBackups() + if err != nil { + metrics.CountError("cleanup") + log.Errorw("cleaning up backups failed", "error", err) + } else { + log.Infow("cleaned up backups") + } + + return nil +} diff --git a/cmd/internal/initializer/initializer.go b/cmd/internal/initializer/initializer.go index 67bc5f2..56527e9 100644 --- a/cmd/internal/initializer/initializer.go +++ b/cmd/internal/initializer/initializer.go @@ -10,9 +10,11 @@ import ( "strings" 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" "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/metrics" "github.com/metal-stack/backup-restore-sidecar/pkg/constants" "go.uber.org/zap" @@ -31,10 +33,11 @@ type Initializer struct { db database.Database bp providers.BackupProvider comp *compress.Compressor + metrics *metrics.Metrics dbDataDir string } -func New(log *zap.SugaredLogger, addr string, db database.Database, bp providers.BackupProvider, comp *compress.Compressor, dbDataDir string) *Initializer { +func New(log *zap.SugaredLogger, addr string, db database.Database, bp providers.BackupProvider, comp *compress.Compressor, metrics *metrics.Metrics, dbDataDir string) *Initializer { return &Initializer{ currentStatus: &v1.StatusResponse{ Status: v1.StatusResponse_CHECKING, @@ -46,6 +49,7 @@ func New(log *zap.SugaredLogger, addr string, db database.Database, bp providers bp: bp, comp: comp, dbDataDir: dbDataDir, + metrics: metrics, } } @@ -68,9 +72,13 @@ func (i *Initializer) Start(ctx context.Context) { initializerService := newInitializerService(i.currentStatus) backupService := newBackupProviderService(i.bp, i.Restore) + databaseService := newDatabaseService(func() error { + return backup.CreateBackup(i.log, i.db, i.bp, i.metrics, i.comp) + }) v1.RegisterInitializerServiceServer(grpcServer, initializerService) v1.RegisterBackupServiceServer(grpcServer, backupService) + v1.RegisterDatabaseServiceServer(grpcServer, databaseService) i.log.Infow("start initializer server", "address", i.addr) diff --git a/cmd/internal/initializer/service.go b/cmd/internal/initializer/service.go index 1a0051b..2b966e9 100644 --- a/cmd/internal/initializer/service.go +++ b/cmd/internal/initializer/service.go @@ -81,3 +81,22 @@ func (s *backupService) RestoreBackup(ctx context.Context, req *v1.RestoreBackup return &v1.RestoreBackupResponse{}, nil } + +type databaseService struct { + backupFn func() error +} + +func newDatabaseService(backupFn func() error) *databaseService { + return &databaseService{ + backupFn: backupFn, + } +} + +func (s *databaseService) CreateBackup(ctx context.Context, _ *v1.Empty) (*v1.CreateBackupResponse, error) { + err := s.backupFn() + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("error creating backup: %s", err)) + } + + return &v1.CreateBackupResponse{}, nil +} diff --git a/cmd/main.go b/cmd/main.go index 0880dc5..4c3dcf6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -139,16 +139,36 @@ var startCmd = &cobra.Command{ if err != nil { return err } - initializer.New(logger.Named("initializer"), addr, db, bp, comp, viper.GetString(databaseDatadirFlg)).Start(stop) + + metrics := metrics.New() + metrics.Start(logger.Named("metrics")) + + initializer.New(logger.Named("initializer"), addr, db, bp, comp, metrics, viper.GetString(databaseDatadirFlg)).Start(stop) if err := probe.Start(stop, logger.Named("probe"), db); err != nil { return err } - metrics := metrics.New() - metrics.Start(logger.Named("metrics")) + return backup.Start(stop, logger.Named("backup"), viper.GetString(backupCronScheduleFlg), db, bp, metrics, comp) }, } +var createBackupCmd = &cobra.Command{ + Use: "create-backup", + Short: "create backup takes a database backup out of the regular time schedule", + PreRunE: func(cmd *cobra.Command, args []string) error { + return initBackupProvider() + }, + RunE: func(cmd *cobra.Command, args []string) error { + c, err := client.New(context.Background(), viper.GetString(serverAddrFlg)) + if err != nil { + return fmt.Errorf("error creating client: %w", err) + } + + _, err = c.DatabaseServiceClient().CreateBackup(context.Background(), &v1.Empty{}) + return err + }, +} + var restoreCmd = &cobra.Command{ Use: "restore", Short: "restores a specific backup manually", @@ -233,7 +253,7 @@ func main() { } func init() { - rootCmd.AddCommand(startCmd, waitCmd, restoreCmd) + 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]") diff --git a/deploy/rethinkdb-local.yaml b/deploy/rethinkdb-local.yaml index 9342ab9..0fab527 100644 --- a/deploy/rethinkdb-local.yaml +++ b/deploy/rethinkdb-local.yaml @@ -1,98 +1,158 @@ +# DO NOT EDIT! This is auto-generated by the integration tests +--- +data: + config.yaml: | + --- + bind-addr: 0.0.0.0 + db: rethinkdb + db-data-directory: /data/rethinkdb/ + backup-provider: local + rethinkdb-passwordfile: /rethinkdb-secret/rethinkdb-password.txt + backup-cron-schedule: "*/1 * * * *" + object-prefix: rethinkdb-test + post-exec-cmds: + # IMPORTANT: the --directory needs to point to the exact sidecar data dir, otherwise the database will be restored to the wrong location + - rethinkdb --bind all --directory /data/rethinkdb --initial-password ${RETHINKDB_PASSWORD} +metadata: + creationTimestamp: null + name: backup-restore-sidecar-config-rethinkdb + namespace: test-rethinkdb --- -apiVersion: apps/v1 -kind: StatefulSet metadata: + creationTimestamp: null + name: rethinkdb + namespace: test-rethinkdb +stringData: + rethinkdb-password: test123! +--- +metadata: + creationTimestamp: null labels: app: rethinkdb name: rethinkdb + namespace: test-rethinkdb +spec: + ports: + - name: "10080" + port: 10080 + targetPort: 10080 + - name: "28015" + port: 28015 + targetPort: 28015 + - name: metrics + port: 2112 + targetPort: 2112 + selector: + app: rethinkdb +status: + loadBalancer: {} +--- +metadata: + creationTimestamp: null + labels: + app: rethinkdb + name: rethinkdb + namespace: test-rethinkdb spec: - serviceName: rethinkdb replicas: 1 selector: matchLabels: app: rethinkdb + serviceName: rethinkdb template: metadata: + creationTimestamp: null labels: app: rethinkdb spec: containers: - - image: rethinkdb:2.4.0 - name: rethinkdb - command: + - command: - backup-restore-sidecar - wait env: - name: RETHINKDB_PASSWORD valueFrom: secretKeyRef: - name: rethinkdb key: rethinkdb-password + name: rethinkdb + image: rethinkdb:2.4.0 + name: rethinkdb ports: - containerPort: 8080 - containerPort: 28015 + resources: {} volumeMounts: - mountPath: /data name: rethinkdb - - name: bin-provision + - mountPath: /usr/local/bin/backup-restore-sidecar + name: bin-provision subPath: backup-restore-sidecar - mountPath: /usr/local/bin/backup-restore-sidecar - - name: backup-restore-sidecar-config - mountPath: /etc/backup-restore-sidecar - - image: rethinkdb:2.4.0 - name: backup-restore-sidecar - command: + - mountPath: /etc/backup-restore-sidecar + name: backup-restore-sidecar-config + - command: - backup-restore-sidecar - start - --log-level=debug + image: rethinkdb:2.4.0 + name: backup-restore-sidecar + ports: + - containerPort: 8000 + name: grpc + resources: {} volumeMounts: - - name: rethinkdb - mountPath: /data - - name: rethinkdb-credentials - mountPath: /rethinkdb-secret - - name: backup-restore-sidecar-config - mountPath: /etc/backup-restore-sidecar - - name: bin-provision + - mountPath: /backup + name: backup + - mountPath: /data + name: rethinkdb + - mountPath: /rethinkdb-secret + name: rethinkdb-credentials + - mountPath: /etc/backup-restore-sidecar + name: backup-restore-sidecar-config + - mountPath: /usr/local/bin/backup-restore-sidecar + name: bin-provision subPath: backup-restore-sidecar - mountPath: /usr/local/bin/backup-restore-sidecar - - name: bin-provision + - mountPath: /usr/local/bin/rethinkdb-dump + name: bin-provision subPath: rethinkdb-dump - mountPath: /usr/local/bin/rethinkdb-dump - - name: bin-provision + - mountPath: /usr/local/bin/rethinkdb-restore + name: bin-provision subPath: rethinkdb-restore - mountPath: /usr/local/bin/rethinkdb-restore initContainers: - - name: backup-restore-sidecar-provider - image: ghcr.io/metal-stack/backup-restore-sidecar:latest - imagePullPolicy: IfNotPresent - command: + - command: - cp - /backup-restore-sidecar - /rethinkdb/rethinkdb-dump - /rethinkdb/rethinkdb-restore - /bin-provision - ports: - - containerPort: 2112 + image: ghcr.io/metal-stack/backup-restore-sidecar:latest + imagePullPolicy: IfNotPresent + name: backup-restore-sidecar-provider + resources: {} volumeMounts: - - name: bin-provision - mountPath: /bin-provision + - mountPath: /bin-provision + name: bin-provision volumes: - name: rethinkdb persistentVolumeClaim: claimName: rethinkdb + - name: backup + persistentVolumeClaim: + claimName: backup - name: rethinkdb-credentials secret: - secretName: rethinkdb items: - key: rethinkdb-password path: rethinkdb-password.txt - - name: backup-restore-sidecar-config - configMap: + secretName: rethinkdb + - configMap: name: backup-restore-sidecar-config-rethinkdb - - name: bin-provision - emptyDir: {} + name: backup-restore-sidecar-config + - emptyDir: {} + name: bin-provision + updateStrategy: {} volumeClaimTemplates: - metadata: + creationTimestamp: null name: rethinkdb spec: accessModes: @@ -100,49 +160,17 @@ spec: resources: requests: storage: 1Gi ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: backup-restore-sidecar-config-rethinkdb -data: - config.yaml: | - db: rethinkdb - db-data-directory: /data/rethinkdb/ - backup-provider: local - rethinkdb-passwordfile: /rethinkdb-secret/rethinkdb-password.txt - backup-cron-schedule: "*/1 * * * *" - object-prefix: rethinkdb-test - post-exec-cmds: - # IMPORTANT: the --directory needs to point to the exact sidecar data dir, otherwise the database will be restored to the wrong location - - rethinkdb --bind all --directory /data/rethinkdb --initial-password ${RETHINKDB_PASSWORD} ---- -apiVersion: v1 -kind: Secret -metadata: - name: rethinkdb - labels: - app: rethinkdb -type: Opaque -stringData: - rethinkdb-password: "test123!" ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app: rethinkdb - name: rethinkdb -spec: - ports: - - name: "10080" - port: 10080 - targetPort: 8080 - - name: "28015" - port: 28015 - targetPort: 28015 - - name: "metrics" - port: 2112 - targetPort: 2112 - selector: - app: rethinkdb + status: {} + - metadata: + creationTimestamp: null + name: backup + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + status: {} +status: + availableReplicas: 0 + replicas: 0 diff --git a/deploy/rethinkdb-test.yaml b/deploy/rethinkdb-test.yaml deleted file mode 100644 index 9289116..0000000 --- a/deploy/rethinkdb-test.yaml +++ /dev/null @@ -1,177 +0,0 @@ -# DO NOT EDIT! This is auto-generated by the integration tests ---- -data: - config.yaml: | - --- - bind-addr: 0.0.0.0 - db: rethinkdb - db-data-directory: /data/rethinkdb/ - backup-provider: local - rethinkdb-passwordfile: /rethinkdb-secret/rethinkdb-password.txt - backup-cron-schedule: "*/1 * * * *" - object-prefix: rethinkdb-test - post-exec-cmds: - # IMPORTANT: the --directory needs to point to the exact sidecar data dir, otherwise the database will be restored to the wrong location - - rethinkdb --bind all --directory /data/rethinkdb --initial-password ${RETHINKDB_PASSWORD} -metadata: - creationTimestamp: null - name: backup-restore-sidecar-config-rethinkdb - namespace: test-rethinkdb ---- -metadata: - creationTimestamp: null - name: rethinkdb - namespace: test-rethinkdb -stringData: - rethinkdb-password: test123! ---- -metadata: - creationTimestamp: null - labels: - app: rethinkdb - name: rethinkdb - namespace: test-rethinkdb -spec: - ports: - - name: "10080" - port: 10080 - targetPort: 10080 - - name: "28015" - port: 28015 - targetPort: 28015 - - name: metrics - port: 2112 - targetPort: 2112 - selector: - app: rethinkdb -status: - loadBalancer: {} ---- -metadata: - creationTimestamp: null - labels: - app: rethinkdb - name: rethinkdb - namespace: test-rethinkdb -spec: - replicas: 1 - selector: - matchLabels: - app: rethinkdb - serviceName: rethinkdb - template: - metadata: - creationTimestamp: null - labels: - app: rethinkdb - spec: - containers: - - command: - - backup-restore-sidecar - - wait - env: - - name: RETHINKDB_PASSWORD - valueFrom: - secretKeyRef: - key: rethinkdb-password - name: rethinkdb - image: rethinkdb:2.4.0 - name: rethinkdb - ports: - - containerPort: 8080 - - containerPort: 28015 - resources: {} - volumeMounts: - - mountPath: /data - name: rethinkdb - - 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: rethinkdb:2.4.0 - name: backup-restore-sidecar - ports: - - containerPort: 8000 - name: grpc - resources: {} - volumeMounts: - - mountPath: /backup - name: backup - - mountPath: /data - name: rethinkdb - - mountPath: /rethinkdb-secret - name: rethinkdb-credentials - - mountPath: /etc/backup-restore-sidecar - name: backup-restore-sidecar-config - - mountPath: /usr/local/bin/backup-restore-sidecar - name: bin-provision - subPath: backup-restore-sidecar - - mountPath: /usr/local/bin/rethinkdb-dump - name: bin-provision - subPath: rethinkdb-dump - - mountPath: /usr/local/bin/rethinkdb-restore - name: bin-provision - subPath: rethinkdb-restore - hostNetwork: true - initContainers: - - command: - - cp - - /backup-restore-sidecar - - /rethinkdb/rethinkdb-dump - - /rethinkdb/rethinkdb-restore - - /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: rethinkdb - persistentVolumeClaim: - claimName: rethinkdb - - name: backup - persistentVolumeClaim: - claimName: backup - - name: rethinkdb-credentials - secret: - items: - - key: rethinkdb-password - path: rethinkdb-password.txt - secretName: rethinkdb - - configMap: - name: backup-restore-sidecar-config-rethinkdb - name: backup-restore-sidecar-config - - emptyDir: {} - name: bin-provision - updateStrategy: {} - volumeClaimTemplates: - - metadata: - creationTimestamp: null - name: rethinkdb - 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 diff --git a/integration/main_test.go b/integration/main_test.go index 33a1016..f0038bb 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -15,6 +15,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/yaml" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -87,7 +88,12 @@ func dumpToExamples(t *testing.T, name string, resources ...client.Object) { `) for i, r := range resources { - r := r + r := r.DeepCopyObject() + + if sts, ok := r.(*appsv1.StatefulSet); ok { + // host network is only for integration testing purposes + sts.Spec.Template.Spec.HostNetwork = false + } raw, err := yaml.Marshal(r) require.NoError(t, err) diff --git a/integration/rethinkdb_test.go b/integration/rethinkdb_test.go index 0e19de8..1847e4c 100644 --- a/integration/rethinkdb_test.go +++ b/integration/rethinkdb_test.go @@ -9,6 +9,7 @@ import ( v1 "github.com/metal-stack/backup-restore-sidecar/api/v1" "github.com/metal-stack/backup-restore-sidecar/pkg/constants" "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -55,7 +56,7 @@ func Test_RethinkDB(t *testing.T) { require.NoError(t, err, "cleanup did not succeed") } cleanup() - // defer cleanup() + defer cleanup() err := c.Create(ctx, ns) require.NoError(t, client.IgnoreAlreadyExists(err)) @@ -352,7 +353,7 @@ post-exec-cmds: t.Log("applying resource manifests") objects := []client.Object{cm(), secret(), service(), sts()} - dumpToExamples(t, "rethinkdb-test.yaml", objects...) + dumpToExamples(t, "rethinkdb-local.yaml", objects...) for _, o := range objects { o := o err = c.Create(ctx, o) @@ -380,7 +381,7 @@ post-exec-cmds: } return nil - }, retry.Context(ctx), retry.Attempts(0)) + }, retry.Context(ctx)) require.NoError(t, err) _, _ = r.DBDrop(db).RunWrite(session) @@ -410,11 +411,14 @@ post-exec-cmds: require.NoError(t, err) require.Equal(t, "i am precious", d1.Data) - t.Log("waiting for backup to be created") + t.Log("taking a backup") brsc, err := brsclient.New(ctx, "http://localhost:8000") require.NoError(t, err) + _, err = brsc.DatabaseServiceClient().CreateBackup(ctx, &v1.Empty{}) + assert.NoError(t, err) + var backup *v1.Backup err = retry.Do(func() error { backups, err := brsc.BackupServiceClient().ListBackups(ctx, &v1.Empty{}) @@ -464,11 +468,21 @@ post-exec-cmds: t.Log("verify that data gets restored") - cursor, err = r.DB(db).Table(table).Get("1").Run(session) - require.NoError(t, err) - var d2 testData - err = cursor.One(&d2) + err = retry.Do(func() error { + cursor, err := r.DB(db).Table(table).Get("1").Run(session) + if err != nil { + return err + } + + err = cursor.One(&d2) + if err != nil { + return err + } + + return nil + }, retry.Context(ctx), retry.Attempts(0), retry.MaxDelay(2*time.Second)) require.NoError(t, err) + require.Equal(t, "i am precious", d2.Data) } diff --git a/pkg/client/client.go b/pkg/client/client.go index dca43be..4d66046 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -13,6 +13,7 @@ import ( type Client interface { InitializerServiceClient() v1.InitializerServiceClient BackupServiceClient() v1.BackupServiceClient + DatabaseServiceClient() v1.DatabaseServiceClient } type client struct { @@ -49,3 +50,7 @@ func (c *client) InitializerServiceClient() v1.InitializerServiceClient { func (c *client) BackupServiceClient() v1.BackupServiceClient { return v1.NewBackupServiceClient(c.conn) } + +func (c *client) DatabaseServiceClient() v1.DatabaseServiceClient { + return v1.NewDatabaseServiceClient(c.conn) +} diff --git a/proto/v1/database.proto b/proto/v1/database.proto new file mode 100644 index 0000000..862357c --- /dev/null +++ b/proto/v1/database.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package v1; + +import "v1/common.proto"; + +service DatabaseService { + rpc CreateBackup(Empty) returns (CreateBackupResponse); +} + +message CreateBackupResponse {}