9c6fc8cffa
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
160 lines
4.3 KiB
Go
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
|
|
},
|
|
}
|