package registry import ( "context" "database/sql" "encoding/json" "errors" "fmt" "reflect" ) // RebindClaimRows transfers an exact stopped installation's registry rows. // The caller holds hostguard and proves the source cut and successor admission. // A transaction preserves row contents and refuses any concurrent replacement. func (r *Registry) RebindClaimRows(ctx context.Context, old, next ColdRows) error { if r.remote != nil { _, err := r.remote.call(ctx, remoteRequest{Method: "RebindClaimRows", Old: old, NextValue: next}, nil) return err } if _, err := r.sql.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS recovery_rows(id TEXT PRIMARY KEY,body BLOB NOT NULL)`); err != nil { return err } tx, err := r.beginClaimTransaction(ctx, true) if err != nil { return err } defer tx.Rollback() id := fmt.Sprintf("%s/%s/%d", next.Route.Host, next.Route.ClaimIncarnation, next.Route.ClaimStamp) var saved []byte err = tx.QueryRowContext(ctx, `SELECT body FROM recovery_rows WHERE id=?`, id).Scan(&saved) recorded := err == nil if recorded { var rows struct{ Old, Next ColdRows } if json.Unmarshal(saved, &rows) != nil || withoutBuildStateCopy(rows.Old.Route) != withoutBuildStateCopy(old.Route) || !reflect.DeepEqual(coldRowsWithoutBuildStateCopy(rows.Next), coldRowsWithoutBuildStateCopy(next)) { return ErrClaimOwnership } old = rows.Old } else if !errors.Is(err, sql.ErrNoRows) { return err } if !old.Route.ClaimOwnership.Valid() || !next.Route.ClaimOwnership.Valid() || old.Route.Host != next.Route.Host || old.Route.ClaimIncarnation != next.Route.ClaimIncarnation || next.Route.ClaimStamp <= old.Route.ClaimStamp || len(old.Timers) != len(next.Timers) { return ErrClaimOwnership } a, b := withoutBuildStateCopy(old.Route), withoutBuildStateCopy(next.Route) a.ClaimOwnership, b.ClaimOwnership = ClaimOwnership{}, ClaimOwnership{} if a != b { return ErrClaimOwnership } for i, t := range next.Timers { prior := old.Timers[i] if !prior.ClaimOwnership.Valid() || prior.Host != old.Route.Host || prior.ClaimIncarnation != old.Route.ClaimIncarnation || prior.ClaimStamp != old.Route.ClaimStamp || prior.ClaimOwner != old.Route.ClaimOwner { return ErrClaimOwnership } a, b := prior, t a.ClaimOwnership, b.ClaimOwnership = ClaimOwnership{}, ClaimOwnership{} if a != b || !t.ClaimOwnership.Valid() || t.Host != next.Route.Host || t.ClaimIncarnation != next.Route.ClaimIncarnation || t.ClaimStamp != next.Route.ClaimStamp || t.ClaimOwner != next.Route.ClaimOwner { return ErrClaimOwnership } } row, err := scanRoute(tx.QueryRowContext(ctx, `SELECT `+routeCols+` FROM routes WHERE host=?`, old.Route.Host).Scan) if err != nil { return err } row = withoutBuildStateCopy(row) replay := row == withoutBuildStateCopy(next.Route) if !replay && row != withoutBuildStateCopy(old.Route) { return ErrClaimOwnership } rows, err := tx.conn.QueryContext(ctx, `SELECT `+timerCols+` FROM timers WHERE host=? ORDER BY name`, old.Route.Host) if err != nil { return err } var timers []Timer for rows.Next() { t, e := scanTimer(rows) if e != nil { rows.Close() return e } timers = append(timers, t) } err = rows.Err() rows.Close() if err != nil { return err } want := old.Timers if replay { want = next.Timers } if len(timers) != len(want) { return ErrClaimOwnership } for _, t := range timers { found := false for _, w := range want { if reflect.DeepEqual(t, w) { found = true break } } if !found { return ErrClaimOwnership } } if replay { if !recorded { return ErrClaimOwnership } return nil } o := next.Route.ClaimOwnership if _, err = tx.ExecContext(ctx, `UPDATE routes SET claim_incarnation=?,claim_stamp=?,claim_owner=?,claim_row_id=? WHERE host=?`, o.ClaimIncarnation, o.ClaimStamp, o.ClaimOwner, o.ClaimRowID, next.Route.Host); err != nil { return err } for _, t := range next.Timers { o := t.ClaimOwnership if _, err = tx.ExecContext(ctx, `UPDATE timers SET claim_incarnation=?,claim_stamp=?,claim_owner=?,claim_row_id=? WHERE host=? AND name=?`, o.ClaimIncarnation, o.ClaimStamp, o.ClaimOwner, o.ClaimRowID, t.Host, t.Name); err != nil { return err } } body, err := json.Marshal(struct{ Old, Next ColdRows }{old, next}) if err != nil { return err } if _, err = tx.ExecContext(ctx, `INSERT INTO recovery_rows(id,body) VALUES(?,?)`, id, body); err != nil { return err } return tx.Commit() }