Files
go-synapse-backupper/cmd/synapse-backupper/backup.go
T

160 lines
4.3 KiB
Go

package main
import (
"context"
"crypto/rand"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/spf13/cobra"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/composite"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
pgdumpadapter "git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pgdump"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/pipeline"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/retention"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/storage/local"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/backup"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
)
var (
newKeyManager = keymanager.NewKeyManager
newLocalSink = local.NewLocalSink
newRunner = defaultNewRunner
outputWriter io.Writer = os.Stderr
)
func defaultNewRunner(options ...pipeline.Option) pipeline.Runner {
return pipeline.NewRunner(options...)
}
func init() {
config.RegisterFlags(backupCmd)
}
var backupCmd = &cobra.Command{
Use: "backup",
Short: "Run a one-off backup",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if ctx == nil {
ctx = context.Background()
}
startTime := time.Now().UTC()
runID, err := uuid.NewRandom()
if err != nil {
return fmt.Errorf("generate run-id: %w", err)
}
logger := slog.New(slog.NewTextHandler(outputWriter, nil))
logger.Info(
"backup started",
slog.String("run_id", runID.String()),
slog.Time("start_time", startTime),
)
cfg, err := config.Load(cmd)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
keyManager := newKeyManager(crypto.NewRegistry())
pqPub, err := keyManager.LoadPub(cfg.PQPublicKeyPath, cfg.PQScheme)
if err != nil {
return fmt.Errorf("load PQ public key: %w", err)
}
classicalPub, err := keyManager.LoadPub(cfg.ClassicalPublicKeyPath, cfg.ClassicalScheme)
if err != nil {
return fmt.Errorf("load classical public key: %w", err)
}
recipients := make([]crypto.RecipientPub, 0, 2)
recipients = append(recipients, pqPub, classicalPub)
pgDumpOpts := pgdump.Options{
Host: cfg.PG.Host,
Port: cfg.PG.Port,
Database: cfg.PG.Database,
User: cfg.PG.User,
Password: cfg.PG.Password,
Key: backup.ArtifactKey(startTime),
ExcludeTables: cfg.PG.ExcludeTables,
}
finalPath := filepath.Join(cfg.Backup.Dir, pgDumpOpts.Key)
logger.Info(
"backup destination",
slog.String("backup_dir", cfg.Backup.Dir),
slog.String("output_path", finalPath),
)
var sink domain.Sink = newLocalSink(cfg.Backup.Dir)
registry := crypto.NewRegistry()
_ = registry.Register(0x0006, func() crypto.KEM { return mlkem768.New() })
_ = registry.Register(0x0007, func() crypto.KEM { return x25519.New() })
runner := newRunner(
pipeline.WithDumper(pgdumpadapter.New()),
pipeline.WithEncryptor(composite.NewEncryptor(registry)),
)
err = runner.Run(ctx, pgDumpOpts, recipients, sink, rand.Reader)
endTime := time.Now().UTC()
var byteCount int64
if info, statErr := os.Stat(finalPath); statErr == nil {
byteCount = info.Size()
}
if err != nil {
logger.Error(
"backup failed",
slog.String("run_id", runID.String()),
slog.String("output_path", finalPath),
slog.Time("start_time", startTime),
slog.Time("end_time", endTime),
slog.Int64("byte_count", byteCount),
slog.String("error", err.Error()),
)
return err
}
logger.Info(
"backup completed",
slog.String("run_id", runID.String()),
slog.String("output_path", finalPath),
slog.Time("start_time", startTime),
slog.Time("end_time", endTime),
slog.Int64("byte_count", byteCount),
)
if _, pruneErr := retention.PruneByAge(
ctx,
cfg.Backup.Dir,
cfg.Backup.RetentionDays,
time.Now(),
); pruneErr != nil {
logger.Error(
"retention pruning failed",
slog.String("error", pruneErr.Error()),
)
return pruneErr
}
return nil
},
}