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--