mirror of
https://github.com/quay/quay.git
synced 2026-10-24 21:59:23 +03:00
* PROJQUAY-13328: fix(migrate): roll back the source registry when a migration fails On the SQLite path a failure after the source services stopped returned with the old registry still down. Restart the stopped source services on any failure after the stop, remove a target install only when this run created it, and keep the migration marker so a rerun resumes the copy. A marker-bearing target now counts as unfinished, and a successful -skip-install run clears the marker. QUAY_MIGRATE_FAIL_AT injects a failure at after-convert (PostgreSQL), after-copy, after-schema or at-install; the e2e-migrate jobs use it to check rollback and retry on both paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PROJQUAY-13328: fix(migrate): remove persisted key files on rollback A root 0600 quay.pem left in the old OMR's config dir is unreadable to quay-app, so its config validator drops every cert and the OMR exits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PROJQUAY-13328: test(migrate): re-capture source JWT key after rollback restarts Each injected-failure rollback restarts the old OMR, which comes back with a new registry JWT key, so the pre-loop fingerprint no longer matches the source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PROJQUAY-13328: fix(migrate): record config material path before persisting it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PROJQUAY-13328: fix(migrate): bound each rollback step with its own timeout Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PROJQUAY-13328: fix(migrate): remove recovered key files on pre-stop failure Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PROJQUAY-13328: fix(migrate): refuse to resume over a completed install Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PROJQUAY-13328: fix(migrate): report a failed migration-marker removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
612 lines
20 KiB
Go
612 lines
20 KiB
Go
package migrate
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/quay/quay/internal/dal/dbcore"
|
|
)
|
|
|
|
func TestPostgresLoopbackURI(t *testing.T) {
|
|
const raw = "postgresql://user:p%40ss@quay-postgres:5432/quay"
|
|
got, err := postgresLoopbackURI(raw)
|
|
if err != nil {
|
|
t.Fatalf("postgresLoopbackURI: %v", err)
|
|
}
|
|
u, err := url.Parse(got)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if u.Hostname() != "127.0.0.1" || u.Port() != "5432" || u.Path != "/quay" {
|
|
t.Fatalf("rewritten URI changed more than the host: %s", u.Redacted())
|
|
}
|
|
password, _ := u.User.Password()
|
|
if u.User.Username() != "user" || password != "p@ss" {
|
|
t.Fatal("rewritten URI changed credentials")
|
|
}
|
|
}
|
|
|
|
func TestPostgresLoopbackURIRejectsUnsupportedEndpointWithoutLeakingPassword(t *testing.T) {
|
|
const secret = "do-not-print-this"
|
|
_, err := postgresLoopbackURI("postgresql://user:" + secret + "@other-postgres/quay")
|
|
if err == nil {
|
|
t.Fatal("unsupported PostgreSQL endpoint was accepted")
|
|
}
|
|
if strings.Contains(err.Error(), secret) {
|
|
t.Fatalf("error leaked password: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPostgresLoopbackURIRejectsQueryOverrides(t *testing.T) {
|
|
_, err := postgresLoopbackURI("postgresql://user:password@quay-postgres/quay?host=other-postgres")
|
|
if err == nil {
|
|
t.Fatal("DB_URI query override was accepted")
|
|
}
|
|
}
|
|
|
|
func TestPostgresConnectionErrorPreservesRedactedCause(t *testing.T) {
|
|
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
accepted := make(chan struct{})
|
|
go func() {
|
|
defer close(accepted)
|
|
conn, err := listener.Accept()
|
|
if err == nil {
|
|
_ = conn.Close()
|
|
}
|
|
}()
|
|
t.Cleanup(func() { _ = listener.Close() })
|
|
|
|
const secret = "sentinel-password-must-not-appear"
|
|
ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second)
|
|
defer cancel()
|
|
conn, err := pgx.Connect(ctx, fmt.Sprintf("postgresql://quay:%s@%s/quay?sslmode=disable", secret, listener.Addr()))
|
|
if conn != nil {
|
|
_ = conn.Close(context.Background())
|
|
}
|
|
if err == nil {
|
|
t.Fatal("pgx.Connect unexpectedly succeeded")
|
|
}
|
|
got := postgresConnectionError(err)
|
|
if strings.Contains(got.Error(), secret) {
|
|
t.Fatalf("connection error leaked password: %v", got)
|
|
}
|
|
if got.Error() == "connect to source PostgreSQL: connection failed" {
|
|
t.Fatalf("connection error discarded safe cause: %v", got)
|
|
}
|
|
select {
|
|
case <-accepted:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("test PostgreSQL listener did not accept a connection")
|
|
}
|
|
|
|
got = postgresConnectionError(errors.New("untrusted error containing " + secret))
|
|
if strings.Contains(got.Error(), secret) {
|
|
t.Fatalf("unclassified connection error leaked password: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestCreateIntermediateDatabaseFileUsesPrivatePermissions(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), partialDatabaseName)
|
|
if err := createIntermediateDatabaseFile(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := info.Mode().Perm(); got != 0o600 {
|
|
t.Fatalf("intermediate database permissions = %04o, want 0600", got)
|
|
}
|
|
if err := createIntermediateDatabaseFile(path); err == nil {
|
|
t.Fatal("createIntermediateDatabaseFile unexpectedly overwrote an existing file")
|
|
}
|
|
}
|
|
|
|
func TestInPostgresNetworkNamespace(t *testing.T) {
|
|
m := &Migrator{Source: OMRSource{PodSandboxKey: "/proc/self/ns/net"}}
|
|
inside, err := m.inPostgresNetworkNamespace()
|
|
if err != nil || !inside {
|
|
t.Fatalf("inPostgresNetworkNamespace = %t, %v", inside, err)
|
|
}
|
|
}
|
|
|
|
func TestRunPostgresCopyInNamespaceBuildsRootlessAndRootfulCommands(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
scope string
|
|
want string
|
|
}{
|
|
{name: "rootless", scope: scopeUser, want: "podman unshare nsenter --net=/run/netns/quay"},
|
|
{name: "rootful", scope: scopeSystem, want: "nsenter --net=/run/netns/quay"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
runner := &recordingRunner{}
|
|
m := &Migrator{
|
|
DataDir: "/target",
|
|
Runner: runner,
|
|
Source: OMRSource{
|
|
ConfigDir: "/source/config",
|
|
SystemdScope: tc.scope,
|
|
PodSandboxKey: "/run/netns/quay",
|
|
},
|
|
}
|
|
if err := m.runPostgresCopyInNamespace(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(runner.runCalls) != 1 || !strings.HasPrefix(runner.runCalls[0], tc.want) {
|
|
t.Fatalf("namespace command = %v, want prefix %q", runner.runCalls, tc.want)
|
|
}
|
|
if !strings.Contains(runner.runCalls[0], "migrate -data-dir=/target -source-certs=/source/config") {
|
|
t.Fatalf("namespace argv did not preserve migrate paths: %s", runner.runCalls[0])
|
|
}
|
|
if strings.Contains(runner.runCalls[0], "postgresql://") {
|
|
t.Fatalf("namespace argv contains database URI: %s", runner.runCalls[0])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunPostgresMigrationStopsOnlyAppBeforeConversionAndRollsBack(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
m.Source.PodSandboxKey = "/run/netns/quay"
|
|
runner := &sequenceRunner{
|
|
namespaceErr: errors.New("conversion failed"),
|
|
namespaceHook: func() {
|
|
partialPath := m.postgresPartialDBPath()
|
|
for _, path := range []string{partialPath, partialPath + "-wal", partialPath + "-shm", filepath.Join(m.DataDir, "quay.db")} {
|
|
if err := os.WriteFile(path, []byte("failed migration artifact"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
m.Runner = runner
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "conversion failed") {
|
|
t.Fatalf("runPostgresMigration error = %v", err)
|
|
}
|
|
want := []string{
|
|
"systemctl --user stop quay-app.service",
|
|
"podman unshare nsenter --net=/run/netns/quay",
|
|
"systemctl --user start quay-pod.service",
|
|
"systemctl --user start quay-postgres.service",
|
|
"systemctl --user start quay-redis.service",
|
|
"systemctl --user start quay-app.service",
|
|
}
|
|
if len(runner.calls) != len(want) {
|
|
t.Fatalf("calls = %v, want %v", runner.calls, want)
|
|
}
|
|
for i := range want {
|
|
if !strings.HasPrefix(runner.calls[i], want[i]) {
|
|
t.Errorf("call %d = %q, want prefix %q", i, runner.calls[i], want[i])
|
|
}
|
|
}
|
|
for _, call := range runner.calls[:2] {
|
|
if strings.Contains(call, "quay-postgres.service") {
|
|
t.Fatalf("PostgreSQL stopped before conversion: %v", runner.calls)
|
|
}
|
|
}
|
|
partialPath := m.postgresPartialDBPath()
|
|
for _, path := range []string{filepath.Join(m.DataDir, markerFile), partialPath, partialPath + "-wal", partialPath + "-shm"} {
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Errorf("rollback left migration artifact %s: %v", path, err)
|
|
}
|
|
}
|
|
if _, err := os.Stat(filepath.Join(m.DataDir, "quay.db")); err != nil {
|
|
t.Fatalf("rollback removed published target database: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemovePostgresMigrationArtifactsRetainsMarkerWhenRetryIsUnsafe(t *testing.T) {
|
|
m := &Migrator{DataDir: t.TempDir()}
|
|
partialPath := m.postgresPartialDBPath()
|
|
markerPath := filepath.Join(m.DataDir, markerFile)
|
|
configPath := filepath.Join(m.DataDir, runtimeConfigFile)
|
|
for _, path := range []string{markerPath, partialPath, partialPath + "-wal", partialPath + "-shm", configPath} {
|
|
if err := os.WriteFile(path, []byte("migration artifact"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if err := m.removePostgresMigrationArtifacts(false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range []string{markerPath, configPath} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Errorf("rollback removed fail-closed artifact %s: %v", path, err)
|
|
}
|
|
}
|
|
for _, path := range []string{partialPath, partialPath + "-wal", partialPath + "-shm"} {
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Errorf("rollback left disposable artifact %s: %v", path, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidatePostgresBeforeStopRejectsResume(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
if err := os.MkdirAll(m.DataDir, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(m.DataDir, markerFile), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err := m.validatePostgresBeforeStop(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "does not support resuming") {
|
|
t.Fatalf("validatePostgresBeforeStop error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPostgresRejectsUncleanTargetDirBeforeStoppingOMR(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
fileNames []string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "stale non-marker file",
|
|
fileNames: []string{"quay.db.partial"},
|
|
wantErr: "target directory",
|
|
},
|
|
{
|
|
name: "corrupted leftovers",
|
|
fileNames: []string{"some-stale-file.db", "storage"},
|
|
wantErr: "target directory",
|
|
},
|
|
{
|
|
name: "stale marker file",
|
|
fileNames: []string{markerFile},
|
|
wantErr: "does not support resuming",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
runner := &recordingRunner{}
|
|
m.Runner = runner
|
|
|
|
if err := os.MkdirAll(m.DataDir, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range tc.fileNames {
|
|
if err := os.WriteFile(filepath.Join(m.DataDir, name), []byte("stale"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Fatalf("runPostgresMigration error = %v, want substring %q", err, tc.wantErr)
|
|
}
|
|
if len(runner.runCalls) != 0 {
|
|
t.Fatalf("OMR was touched before unclean target-dir rejection: %v", runner.runCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPostgresRejectsExistingTargetQuadletBeforeStoppingOMR(t *testing.T) {
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
m.checkTargetInstallation = func() (bool, error) { return true, nil }
|
|
runner := &recordingRunner{}
|
|
m.Runner = runner
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "existing target Quay installation") {
|
|
t.Fatalf("runPostgresMigration error = %v", err)
|
|
}
|
|
if len(runner.runCalls) != 0 {
|
|
t.Fatalf("OMR was touched before target rejection: %v", runner.runCalls)
|
|
}
|
|
}
|
|
|
|
func TestPostgresStopErrorStillRestoresOldServices(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
runner := &sequenceRunner{appStopErr: errors.New("stop result unknown")}
|
|
m.Runner = runner
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "stop result unknown") {
|
|
t.Fatalf("runPostgresMigration error = %v", err)
|
|
}
|
|
want := []string{
|
|
"systemctl --user stop quay-app.service",
|
|
"systemctl --user start quay-pod.service",
|
|
"systemctl --user start quay-postgres.service",
|
|
"systemctl --user start quay-redis.service",
|
|
"systemctl --user start quay-app.service",
|
|
}
|
|
if strings.Join(runner.calls, "\n") != strings.Join(want, "\n") {
|
|
t.Fatalf("calls = %v, want %v", runner.calls, want)
|
|
}
|
|
}
|
|
|
|
func TestPostgresUnsupportedFlags(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
set func(*Migrator)
|
|
want string
|
|
}{
|
|
{name: "dry run", set: func(m *Migrator) { m.DryRun = true }, want: "-dry-run"},
|
|
{name: "skip install", set: func(m *Migrator) { m.SkipInstall = true }, want: "-skip-install"},
|
|
{name: "cleanup", set: func(m *Migrator) { m.Cleanup = true }, want: "-cleanup"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m := &Migrator{}
|
|
tc.set(m)
|
|
err := m.validatePostgresFlags()
|
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
|
t.Fatalf("validatePostgresFlags error = %v, want %q", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStopPostgresRemainingServicesAtCutover(t *testing.T) {
|
|
runner := &recordingRunner{}
|
|
m := &Migrator{Runner: runner, Source: OMRSource{SystemdScope: scopeSystem}}
|
|
if err := m.stopPostgresRemainingServices(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []string{
|
|
"systemctl stop quay-postgres.service",
|
|
"systemctl stop quay-redis.service",
|
|
"systemctl stop quay-pod.service",
|
|
}
|
|
if strings.Join(runner.runCalls, "\n") != strings.Join(want, "\n") {
|
|
t.Fatalf("cutover calls = %v, want %v", runner.runCalls, want)
|
|
}
|
|
}
|
|
|
|
func TestUpgradeSchemaAtomicallyPublishesPostgresIntermediate(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
partialPath := filepath.Join(dataDir, partialDatabaseName)
|
|
db, err := dbcore.OpenSQLite(partialPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := dbcore.InitOMRSourceIntermediate(t.Context(), db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m := &Migrator{DataDir: dataDir, Out: &bytes.Buffer{}, Source: OMRSource{DatabaseKind: databasePostgres}}
|
|
if err := m.upgradeSchema(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(partialPath); !os.IsNotExist(err) {
|
|
t.Fatalf("partial database still exists: %v", err)
|
|
}
|
|
if backups, err := filepath.Glob(partialPath + ".backup-*"); err != nil || len(backups) != 0 {
|
|
t.Fatalf("converted database backups = %v, err=%v", backups, err)
|
|
}
|
|
target, err := dbcore.OpenSQLiteReadOnly(filepath.Join(dataDir, "quay.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer target.Close()
|
|
version, err := dbcore.SchemaVersion(t.Context(), target)
|
|
if err != nil || version != dbcore.TargetVersion {
|
|
t.Fatalf("published version = %q, err=%v", version, err)
|
|
}
|
|
}
|
|
|
|
func TestUpgradeSchemaRejectsUnexpectedCurrentPostgresIntermediate(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
partialPath := filepath.Join(dataDir, partialDatabaseName)
|
|
db, err := dbcore.OpenSQLite(partialPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := dbcore.InitDatabase(t.Context(), db, &bytes.Buffer{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m := &Migrator{DataDir: dataDir, Out: &bytes.Buffer{}, Source: OMRSource{DatabaseKind: databasePostgres}}
|
|
err = m.upgradeSchema(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "unexpectedly has target schema version") {
|
|
t.Fatalf("upgradeSchema error = %v", err)
|
|
}
|
|
if _, err := os.Stat(partialPath); err != nil {
|
|
t.Fatalf("unexpected intermediate was removed: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dataDir, "quay.db")); !os.IsNotExist(err) {
|
|
t.Fatalf("unexpected intermediate was published: %v", err)
|
|
}
|
|
}
|
|
|
|
type sequenceRunner struct {
|
|
calls []string
|
|
namespaceErr error
|
|
namespaceHook func()
|
|
appStopErr error
|
|
}
|
|
|
|
func (r *sequenceRunner) Run(_ context.Context, name string, args ...string) error {
|
|
call := name + " " + strings.Join(args, " ")
|
|
r.calls = append(r.calls, call)
|
|
if name == "podman" && len(args) > 0 && args[0] == "unshare" {
|
|
if r.namespaceHook != nil {
|
|
r.namespaceHook()
|
|
}
|
|
return r.namespaceErr
|
|
}
|
|
if strings.Contains(call, " stop quay-app.service") {
|
|
return r.appStopErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (*sequenceRunner) Output(context.Context, string, ...string) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func TestRunPostgresMigrationInjectedFailureMarkerOutcome(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
phase string
|
|
keepMarker bool
|
|
}{
|
|
{phase: "after-convert", keepMarker: false},
|
|
{phase: "after-copy", keepMarker: true},
|
|
} {
|
|
t.Run(tc.phase, func(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
t.Setenv(failAtEnv, tc.phase)
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
m.Source.PodSandboxKey = "/run/netns/quay"
|
|
sourceDB := m.Source.DBPath
|
|
runner := &sequenceRunner{namespaceHook: func() {
|
|
data, err := os.ReadFile(sourceDB)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(m.postgresPartialDBPath(), data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}}
|
|
m.Runner = runner
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "injected failure at "+tc.phase) || !strings.Contains(err.Error(), "old OMR services restarted") {
|
|
t.Fatalf("runPostgresMigration error = %v", err)
|
|
}
|
|
if got := runner.calls[len(runner.calls)-1]; got != "systemctl --user start quay-app.service" {
|
|
t.Fatalf("last call = %q, want quay-app restart; calls = %v", got, runner.calls)
|
|
}
|
|
if _, err := os.Stat(m.postgresPartialDBPath()); !os.IsNotExist(err) {
|
|
t.Errorf("rollback left %s: %v", partialDatabaseName, err)
|
|
}
|
|
_, err = os.Stat(filepath.Join(m.DataDir, markerFile))
|
|
if kept := err == nil; kept != tc.keepMarker {
|
|
t.Errorf("marker kept = %v, want %v (stat: %v)", kept, tc.keepMarker, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPostgresRollbackReportsFailedRestart(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
m.Source.PodSandboxKey = "/run/netns/quay"
|
|
runner := &recordingRunner{runErrs: map[string]error{"quay-postgres.service": errors.New("unit failed")}}
|
|
m.Runner = runner
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil {
|
|
t.Fatal("runPostgresMigration succeeded, want conversion failure")
|
|
}
|
|
for _, want := range []string{"validate converted source", "rollback old OMR", "start quay-postgres.service: unit failed"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("runPostgresMigration error = %v, want %q", err, want)
|
|
}
|
|
}
|
|
if strings.Contains(err.Error(), "restarted") {
|
|
t.Errorf("runPostgresMigration error = %v, claims a restart that failed", err)
|
|
}
|
|
if got := runner.runCalls[len(runner.runCalls)-1]; got != "systemctl --user start quay-app.service" {
|
|
t.Fatalf("rollback stopped at a failed restart: %v", runner.runCalls)
|
|
}
|
|
}
|
|
|
|
func TestPostgresRollbackGivesEachStepItsOwnTimeout(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
original := rollbackStepTimeout
|
|
rollbackStepTimeout = 50 * time.Millisecond
|
|
t.Cleanup(func() { rollbackStepTimeout = original })
|
|
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
m.Source.PodSandboxKey = "/run/netns/quay"
|
|
runner := &slowRollbackRunner{slowStart: quayPodUnit}
|
|
m.Runner = runner
|
|
m.removeTargetInstall = func(ctx context.Context) error {
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "start quay-pod.service") {
|
|
t.Fatalf("runPostgresMigration error = %v, want slow quay-pod restart reported", err)
|
|
}
|
|
if !runner.appStarted || runner.appCtxErr != nil {
|
|
t.Fatalf("quay-app restart started = %v with context error %v, want a live context", runner.appStarted, runner.appCtxErr)
|
|
}
|
|
}
|
|
|
|
func TestPostgresValidateFailureRemovesRecoveredKeyMaterial(t *testing.T) {
|
|
t.Setenv("HOME", t.TempDir())
|
|
m := validInstallMigrator(t)
|
|
m.Source.DatabaseKind = databasePostgres
|
|
m.Source.SystemdScope = scopeUser
|
|
outputs := map[string]string{}
|
|
for _, name := range []string{legacyPrivateKeyName, legacyKeyIDName} {
|
|
path := filepath.Join(m.Source.ConfigDir, name)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Remove(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
outputs[filepath.Join(sourceContainerConf, name)] = string(data)
|
|
}
|
|
runner := &containerKeyRunner{material: sourceMaterialRunner{outputs: outputs}}
|
|
m.Runner = runner
|
|
// A missing image fails validation after the key material is recovered.
|
|
m.Source.Image = ""
|
|
|
|
err := m.runPostgresMigration(t.Context())
|
|
if err == nil || !strings.Contains(err.Error(), "no image archive found") {
|
|
t.Fatalf("runPostgresMigration error = %v, want missing image rejection", err)
|
|
}
|
|
if runner.material.outputCalls == 0 {
|
|
t.Fatal("key material was not recovered from the source container")
|
|
}
|
|
for _, name := range []string{legacyPrivateKeyName, legacyKeyIDName} {
|
|
if _, err := os.Stat(filepath.Join(m.Source.ConfigDir, name)); !os.IsNotExist(err) {
|
|
t.Errorf("recovered %s left in source config dir: %v", name, err)
|
|
}
|
|
}
|
|
if len(runner.runCalls) != 0 {
|
|
t.Errorf("failure before the stop ran source services: %v", runner.runCalls)
|
|
}
|
|
}
|