From fa46733859e368c7433e254b65536496b973463f Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 06:52:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(offsite):=20fetch-db=20latest=20=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=E6=96=B0=E8=A3=85=E6=9C=BA=E5=99=A8=E7=9A=84=E7=A9=BA?= =?UTF-8?q?=E5=BA=93=E5=8C=85=EF=BC=8C=E5=8F=96=E5=8C=85=E5=90=8E=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=8C=85=E5=86=85=E8=B4=A6=E5=8F=B7=E5=92=8C=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/felis/db.go | 9 +- cmd/felis/db_test.go | 23 +++- cmd/felis/offsite.go | 48 ++++--- cmd/felis/offsite_test.go | 197 ++++++++++++++++++++++++++++ docs/troubleshooting.md | 14 +- internal/dbbackup/dbbackup.go | 47 +++++++ internal/dbbackup/dbbackup_test.go | 94 ++++++++++++- internal/dbbackup/restore.go | 44 +++++-- internal/offsite/db_test.go | 203 +++++++++++++++++++++++++++++ internal/offsite/sync.go | 67 ++++++++++ internal/pgint/dbrestore_test.go | 8 ++ 11 files changed, 720 insertions(+), 34 deletions(-) create mode 100644 internal/offsite/db_test.go diff --git a/cmd/felis/db.go b/cmd/felis/db.go index 9ba26b2..e6cbab7 100644 --- a/cmd/felis/db.go +++ b/cmd/felis/db.go @@ -211,8 +211,8 @@ func dbRestore(fs *flag.FlagSet, dir *string, args []string, stdout, stderr io.W return 1 } if !*yes { - fmt.Fprintf(stderr, "felis db restore: this replaces every table in the felis database with %s (%s, taken %s, schema %d).\n", - filepath.Base(bundle), m.Label, m.CreatedAt.Format(time.RFC3339), m.SchemaVersion) + fmt.Fprintf(stderr, "felis db restore: this replaces every table in the felis database with %s (%s, taken %s, schema %d, holding %s).\n", + filepath.Base(bundle), m.Label, m.CreatedAt.Format(time.RFC3339), m.SchemaVersion, m.Counts.String()) fmt.Fprintln(stderr, "Scale felis-api and felis-operator to 0 first, then re-run with -yes.") return 2 } @@ -268,8 +268,9 @@ func dbVerify(fs *flag.FlagSet, dir *string, args []string, stdout, stderr io.Wr fmt.Fprintf(stderr, "felis db verify: %v\n", err) return 1 } - fmt.Fprintf(stdout, "%s: ok\n taken %s (%s)\n felis %s\n schema %d\n %s\n", - filepath.Base(bundle), m.CreatedAt.Format(time.RFC3339), m.Label, orUnknown(m.FelisVersion), m.SchemaVersion, orUnknown(m.PGDumpVersion)) + fmt.Fprintf(stdout, "%s: ok\n taken %s (%s)\n felis %s\n schema %d\n holds %s\n %s\n", + filepath.Base(bundle), m.CreatedAt.Format(time.RFC3339), m.Label, orUnknown(m.FelisVersion), m.SchemaVersion, + m.Counts.String(), orUnknown(m.PGDumpVersion)) for _, f := range m.Files { if f.Link != "" { fmt.Fprintf(stdout, " %-40s -> %s\n", f.Name, f.Link) diff --git a/cmd/felis/db_test.go b/cmd/felis/db_test.go index a381847..39de253 100644 --- a/cmd/felis/db_test.go +++ b/cmd/felis/db_test.go @@ -29,6 +29,26 @@ func TestDBUsage(t *testing.T) { } } +func TestDBVerifySaysWhatTheBundleHolds(t *testing.T) { + dir := newPodRig(t) + cfg := podConfig(t, dir) + bundles := filepath.Join(dir, "bundles") + var out, errBuf bytes.Buffer + if code := run([]string{"db", "backup", "-config", cfg, "-dir", bundles, "-state-dir", "", "-no-servers"}, &out, &errBuf); code != 0 { + t.Fatalf("backup: exit %d: %s", code, errBuf.String()) + } + bundle := strings.TrimSpace(strings.TrimPrefix(out.String(), "felis db backup: wrote ")) + out.Reset() + if code := run([]string{"db", "verify", "-dir", bundles, filepath.Base(bundle)}, &out, &errBuf); code != 0 { + t.Fatalf("verify: exit %d: %s", code, errBuf.String()) + } + for _, want := range []string{filepath.Base(bundle) + ": ok", "schema 3", "holds 4 accounts, 2 servers", "pg_dump (PostgreSQL) 18.6"} { + if !strings.Contains(out.String(), want) { + t.Errorf("verify output lacks %q:\n%s", want, out.String()) + } + } +} + // TestDBRestoreNeedsYes: without -yes a restore describes the bundle and stops // before anything reaches the database, even with -force and // -no-safety-backup, which would otherwise let the replay run at once. @@ -49,7 +69,7 @@ func TestDBRestoreNeedsYes(t *testing.T) { if code != 2 { t.Fatalf("exit %d, want 2; stderr %q", code, errBuf.String()) } - if want := filepath.Base(bundle) + " (manual, taken "; !strings.Contains(errBuf.String(), want) || !strings.Contains(errBuf.String(), "schema 3).") { + if want := filepath.Base(bundle) + " (manual, taken "; !strings.Contains(errBuf.String(), want) || !strings.Contains(errBuf.String(), "schema 3, holding 4 accounts, 2 servers).") { t.Errorf("stderr %q does not describe the bundle", errBuf.String()) } if !strings.Contains(errBuf.String(), "re-run with -yes") { @@ -238,6 +258,7 @@ printf 'PGDMP-fake-archive' cat > /dev/null `, "psql": `#!/bin/sh +for a in "$@"; do case "$a" in *"FROM users"*) echo "4|2"; exit 0 ;; esac; done for a in "$@"; do [ "$a" = "-c" ] && { echo 3; exit 0; }; done cat > /dev/null `, diff --git a/cmd/felis/offsite.go b/cmd/felis/offsite.go index 5ed4933..7f15b4b 100644 --- a/cmd/felis/offsite.go +++ b/cmd/felis/offsite.go @@ -501,10 +501,7 @@ func printOffsiteList(env *offsiteEnv, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "felis offsite list: %v\n", err) return 1 } - fmt.Fprintf(stdout, "database bundles (%d, newest first):\n", len(bundles)) - for _, b := range bundles { - fmt.Fprintf(stdout, " %s %s\n", b.Key, offsite.HumanBytes(b.Size)) - } + printDBBundles(ctx, env.bucket, env.key, bundles, stdout) var total int64 for _, w := range worlds { total += w.Size @@ -541,6 +538,20 @@ func printOffsiteList(env *offsiteEnv, stdout, stderr io.Writer) int { return 0 } +// printDBBundles lists the database bundles with what each one's database +// held, read off the front of each, so a restore can pick one by its contents. +func printDBBundles(ctx context.Context, b offsite.Bucket, key []byte, bundles []offsite.Object, stdout io.Writer) { + fmt.Fprintf(stdout, "database bundles (%d, newest first; restore one with fetch-db):\n", len(bundles)) + for _, o := range bundles { + m, err := offsite.PeekDB(ctx, b, key, o.Key) + if err != nil { + fmt.Fprintf(stdout, " %s %s unreadable: %v\n", o.Key, offsite.HumanBytes(o.Size), err) + continue + } + fmt.Fprintf(stdout, " %s %s %s\n", o.Key, offsite.HumanBytes(o.Size), m.Counts.String()) + } +} + func offsiteFetchDB(fs *flag.FlagSet, args []string, stdout, stderr io.Writer) int { cfgPath := fs.String("config", "/etc/felis/felis.toml", "path to felis.toml; on a host with no install yet, give -endpoint and -bucket instead") envFile := fs.String("env-file", defaultOffsiteEnvFile, "file with the [offsite] secrets, for variables not already set") @@ -583,37 +594,44 @@ func offsiteFetchDB(fs *flag.FlagSet, args []string, stdout, stderr io.Writer) i } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() + return fetchDB(ctx, env.bucket, env.key, arg, *dir, time.Now(), stdout, stderr) +} + +// fetchDB is fetch-db once the bucket is open: arg is a bundle name or latest. +func fetchDB(ctx context.Context, b offsite.Bucket, key []byte, arg, dir string, now time.Time, stdout, stderr io.Writer) int { name := arg if name == "latest" { - bundles, err := offsite.ListDB(ctx, env.bucket) - if err != nil { + var err error + if name, _, err = offsite.ChooseDB(ctx, b, key); err != nil { fmt.Fprintf(stderr, "felis offsite fetch-db: %v\n", err) return 1 } - if len(bundles) == 0 { - fmt.Fprintln(stderr, "felis offsite fetch-db: the bucket holds no database bundle") - return 1 - } - name = bundles[0].Key } if _, _, ok := dbbackup.ParseBundleName(name); !ok { fmt.Fprintf(stderr, "felis offsite fetch-db: %q is not a bundle name (felis-db--