feat: PG remote databases version 18.x support backup and restore (#11048)

Refs #10917
This commit is contained in:
ssongliu 2025-11-24 15:47:14 +08:00 committed by GitHub
parent a853d8c869
commit 7c33dd9026
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 51 additions and 66 deletions

View file

@ -82,7 +82,7 @@ func handlePostgresqlBackup(db DatabaseHelper, parentTask *task.Task, recordID u
}
}
itemHandler := func() error { return doPostgresqlgBackup(db, targetDir, fileName, secret) }
itemHandler := func() error { return doPostgresqlgBackup(db, targetDir, fileName, secret, backupTask) }
if parentTask != nil {
return itemHandler()
}
@ -189,17 +189,19 @@ func handlePostgresqlRecover(req dto.CommonRecover, parentTask *task.Task, isRol
return nil
}
func doPostgresqlgBackup(db DatabaseHelper, targetDir, fileName, secret string) error {
func doPostgresqlgBackup(db DatabaseHelper, targetDir, fileName, secret string, task *task.Task) error {
cli, err := LoadPostgresqlClientByFrom(db.Database)
if err != nil {
return err
}
defer cli.Close()
backupInfo := pgclient.BackupInfo{
Database: db.Database,
Name: db.Name,
TargetDir: targetDir,
FileName: fileName,
Task: task,
Timeout: 300,
}
if err := cli.Backup(backupInfo); err != nil {

View file

@ -178,7 +178,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
}
}
} else {
if err := doPostgresqlgBackup(dbInfo, backupDir, record.FileName, cronjob.Secret); err != nil {
if err := doPostgresqlgBackup(dbInfo, backupDir, record.FileName, cronjob.Secret, taskItem); err != nil {
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
retry++
return err

View file

@ -197,6 +197,8 @@ ErrDatabaseIsExist: 'The current database already exists, please re-enter'
ErrExecTimeOut: 'SQL execution timed out, please check the database'
ErrRemoteExist: 'The remote database already exists with this name, please modify it and try again'
ErrLocalExist: 'The name already exists in the local database, please modify it and try again'
RemoteBackup: "To back up the remote database, the local container database service needs to be started first using the image {{ .name }}, please wait..."
RemoteRecover: "To restore the remote database, the local container database service needs to be started first using the image {{ .name }}, please wait..."
#redis
ErrTypeOfRedis: 'The recovery file type does not match the current persistence method, please modify it and try again'

View file

@ -196,6 +196,8 @@ ErrDatabaseIsExist: 'La base de datos actual ya existe, intente con otro nombre'
ErrExecTimeOut: 'Tiempo de espera en la ejecución SQL, revise la base de datos'
ErrRemoteExist: 'La base de datos remota ya existe con ese nombre, modifíquelo e intente de nuevo'
ErrLocalExist: 'El nombre ya existe en la base de datos local, modifíquelo e intente de nuevo'
RemoteBackup: "Para hacer una copia de seguridad de la base de datos remota, primero debe iniciarse el servicio de base de datos del contenedor local utilizando la imagen {{ .name }}, espere por favor..."
RemoteRecover: "Para restaurar la base de datos remota, primero debe iniciarse el servicio de base de datos del contenedor local utilizando la imagen {{ .name }}, espere por favor..."
#redis
ErrTypeOfRedis: 'El tipo de archivo de recuperación no coincide con el método de persistencia actual, modifíquelo e intente'

View file

@ -196,6 +196,8 @@ ErrDatabaseIsExist: '現在のデータベースは既に存在します。再
ErrExecTimeOut: 'SQL 実行がタイムアウトしました。データベースを確認してください'
ErrRemoteExist: 'この名前のリモート データベースは既に存在します。変更してもう一度お試しください'
ErrLocalExist: '名前はローカル データベースに既に存在します。変更してもう一度お試しください'
RemoteBackup: "リモートデータベースをバックアップするには、まずイメージ {{ .name }} を使用してローカルコンテナデータベースサービスを起動する必要があります。しばらくお待ちください..."
RemoteRecover: "リモートデータベースを復元するには、まずイメージ {{ .name }} を使用してローカルコンテナデータベースサービスを起動する必要があります。しばらくお待ちください..."
#redis
ErrTypeOfRedis: 'リカバリ ファイルの種類が現在の永続化方法と一致しません。変更して再試行してください'

View file

@ -197,6 +197,8 @@ ErrDatabaseIsExist: '현재 데이터베이스가 이미 존재합니다. 다시
ErrExecTimeOut: 'SQL 실행 시간이 초과되었습니다. 데이터베이스를 확인하십시오.'
ErrRemoteExist: '이 이름을 가진 원격 데이터베이스가 이미 존재합니다. 수정하고 다시 시도하세요'
ErrLocalExist: '이름이 로컬 데이터베이스에 이미 존재합니다. 이름을 수정하고 다시 시도하세요'
RemoteBackup: "원격 데이터베이스를 백업하려면 먼저 이미지 {{ .name }}을(를) 사용하여 로컬 컨테이너 데이터베이스 서비스를 시작해야 합니다. 잠시만 기다려 주세요..."
RemoteRecover: "원격 데이터베이스를 복원하려면 먼저 이미지 {{ .name }}을(를) 사용하여 로컬 컨테이너 데이터베이스 서비스를 시작해야 합니다. 잠시만 기다려 주세요..."
#레디스
ErrTypeOfRedis: '복구 파일 유형이 현재 지속성 방법과 일치하지 않습니다. 수정하고 다시 시도하세요'

View file

@ -197,6 +197,8 @@ ErrDatabaseIsExist: 'Pangkalan data semasa sudah wujud, sila masukkan semula'
ErrExecTimeOut: 'Pelaksanaan SQL tamat masa, sila semak pangkalan data'
ErrRemoteExist: 'Pangkalan data jauh sudah wujud dengan nama ini, sila ubah suainya dan cuba lagi'
ErrLocalExist: 'Nama sudah wujud dalam pangkalan data tempatan, sila ubah suai dan cuba lagi'
RemoteBackup: "Untuk menyandarkan pangkalan data jauh, perkhidmatan pangkalan data bekas tempatan perlu dimulakan terlebih dahulu menggunakan imej {{ .name }}, sila tunggu..."
RemoteRecover: "Untuk memulihkan pangkalan data jauh, perkhidmatan pangkalan data bekas tempatan perlu dimulakan terlebih dahulu menggunakan imej {{ .name }}, sila tunggu..."
#redis
ErrTypeOfRedis: 'Jenis fail pemulihan tidak sepadan dengan kaedah kegigihan semasa, sila ubah suai dan cuba lagi'

View file

@ -197,6 +197,8 @@ ErrDatabaseIsExist: 'O banco de dados atual já existe, digite novamente'
ErrExecTimeOut: 'Tempo limite de execução do SQL expirou, verifique o banco de dados'
ErrRemoteExist: 'O banco de dados remoto já existe com este nome, modifique-o e tente novamente'
ErrLocalExist: 'O nome já existe no banco de dados local, modifique-o e tente novamente'
RemoteBackup: "Para fazer backup do banco de dados remoto, o serviço de banco de dados do contêiner local precisa ser iniciado primeiro usando a imagem {{ .name }}, por favor aguarde..."
RemoteRecover: "Para restaurar o banco de dados remoto, o serviço de banco de dados do contêiner local precisa ser iniciado primeiro usando a imagem {{ .name }}, por favor aguarde..."
#redis
ErrTypeOfRedis: 'O tipo de arquivo de recuperação não corresponde ao método de persistência atual, modifique-o e tente novamente'

View file

@ -197,6 +197,8 @@ ErrDatabaseIsExist: 'Текущая база данных уже существ
ErrExecTimeOut: 'Время выполнения SQL истекло, проверьте базу данных'
ErrRemoteExist: 'Удаленная база данных с таким именем уже существует. Измените его и повторите попытку'
ErrLocalExist: 'Имя уже существует в локальной базе данных, измените его и повторите попытку'
RemoteBackup: "Для резервного копирования удаленной базы данных необходимо сначала запустить службу базы данных локального контейнера с помощью образа {{ .name }}, пожалуйста, подождите..."
RemoteRecover: "Для восстановления удаленной базы данных необходимо сначала запустить службу базы данных локального контейнера с помощью образа {{ .name }}, пожалуйста, подождите..."
#редис
ErrTypeOfRedis: 'Тип файла восстановления не соответствует текущему методу сохранения. Измените его и повторите попытку'

View file

@ -198,6 +198,8 @@ ErrDatabaseIsExist: 'Mevcut veritabanı zaten mevcut, lütfen yeniden girin'
ErrExecTimeOut: 'SQL yürütme zaman aşımı, lütfen veritabanını kontrol edin'
ErrRemoteExist: 'Uzak veritabanında bu adla zaten mevcut, lütfen değiştirin ve tekrar deneyin'
ErrLocalExist: 'Yerel veritabanında bu ad zaten mevcut, lütfen değiştirin ve tekrar deneyin'
RemoteBackup: "Uzak veritabanını yedeklemek için önce {{ .name }} görüntüsü kullanılarak yerel konteyner veritabanı hizmetinin başlatılması gerekiyor, lütfen bekleyin..."
RemoteRecover: "Uzak veritabanını geri yüklemek için önce {{ .name }} görüntüsü kullanılarak yerel konteyner veritabanı hizmetinin başlatılması gerekiyor, lütfen bekleyin..."
#redis
ErrTypeOfRedis: 'Kurtarma dosyası türü mevcut kalıcılık yöntemiyle eşleşmiyor, lütfen değiştirin ve tekrar deneyin'

View file

@ -196,6 +196,8 @@ ErrDatabaseIsExist: '目前資料庫已存在,請重新輸入'
ErrExecTimeOut: 'SQL 執行逾時,請檢查資料庫'
ErrRemoteExist: '遠端資料庫已存在該名稱,請修改後重試'
ErrLocalExist: '本機資料庫已存在該名稱,請修改後重試'
RemoteBackup: "備份遠端資料庫需要先使用映像 {{ .name }} 啟動本機容器資料庫服務,請稍候..."
RemoteRecover: "恢復遠端資料庫需要先使用映像 {{ .name }} 啟動本機容器資料庫服務,請稍候..."
#redis
ErrTypeOfRedis: '復原檔案類型與目前持久化方式不符,請修改後重試'

View file

@ -197,6 +197,8 @@ ErrDatabaseIsExist: "当前数据库已存在,请重新输入"
ErrExecTimeOut: "SQL 执行超时,请检查数据库"
ErrRemoteExist: "远程数据库已存在该名称,请修改后重试"
ErrLocalExist: "本地数据库已存在该名称,请修改后重试"
RemoteBackup: "备份远程数据库需要先使用镜像 {{ .name }} 启动本地容器数据库服务,请稍候..."
RemoteRecover: "恢复远程数据库需要先使用镜像 {{ .name }} 启动本地容器数据库服务,请稍候..."
#redis
ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后重试"

View file

@ -1,6 +1,7 @@
package client
import (
"github.com/1Panel-dev/1Panel/agent/app/task"
_ "github.com/jackc/pgx/v5/stdlib"
)
@ -49,19 +50,23 @@ type PasswordChangeInfo struct {
}
type BackupInfo struct {
Database string `json:"database"`
Name string `json:"name"`
TargetDir string `json:"targetDir"`
FileName string `json:"fileName"`
Timeout uint `json:"timeout"` // second
Task *task.Task `json:"-"`
Timeout uint `json:"timeout"` // second
}
type RecoverInfo struct {
Database string `json:"database"`
Name string `json:"name"`
SourceFile string `json:"sourceFile"`
Username string `json:"username"`
Timeout uint `json:"timeout"` // second
Task *task.Task `json:"-"`
Timeout uint `json:"timeout"` // second
}
type SyncDBInfo struct {

View file

@ -8,12 +8,12 @@ import (
"io"
"os"
"os/exec"
"sort"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/docker/docker/api/types/image"
"github.com/pkg/errors"
@ -119,10 +119,11 @@ func (r *Remote) ChangePassword(info PasswordChangeInfo) error {
}
func (r *Remote) Backup(info BackupInfo) error {
imageTag, err := loadImageTag()
imageTag, err := loadImageTag(info.Database)
if err != nil {
return err
}
info.Task.Log(i18n.GetWithName("RemoteBackup", imageTag))
fileOp := files.NewFileOp()
if !fileOp.Stat(info.TargetDir) {
if err := os.MkdirAll(info.TargetDir, os.ModePerm); err != nil {
@ -144,7 +145,7 @@ func (r *Remote) Backup(info BackupInfo) error {
_, _ = handle.Read(b)
if string(b) != string(n) {
errBytes, _ := os.ReadFile(fileNameItem)
return fmt.Errorf("backup failed,err:%s", string(errBytes))
return fmt.Errorf("backup failed, err: %s", string(errBytes))
}
gzipCmd := exec.Command("gzip", fileNameItem)
@ -156,10 +157,11 @@ func (r *Remote) Backup(info BackupInfo) error {
}
func (r *Remote) Recover(info RecoverInfo) error {
imageTag, err := loadImageTag()
imageTag, err := loadImageTag(info.Database)
if err != nil {
return err
}
info.Task.Log(i18n.GetWithName("RemoteRecover", imageTag))
fileName := info.SourceFile
if strings.HasSuffix(info.SourceFile, ".sql.gz") {
fileName = strings.TrimSuffix(info.SourceFile, ".gz")
@ -244,69 +246,24 @@ func (r *Remote) ExecSQL(command string, timeout uint) error {
return nil
}
func loadImageTag() (string, error) {
var (
app model.App
appDetails []model.AppDetail
versions []string
)
if err := global.DB.Where("key = ?", "postgresql").First(&app).Error; err != nil {
versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"}
} else {
if err := global.DB.Where("app_id = ?", app.ID).Find(&appDetails).Error; err != nil {
versions = []string{"postgres:16.1-alpine", "postgres:16.0-alpine"}
} else {
for _, item := range appDetails {
versions = append(versions, "postgres:"+item.Version)
}
}
func loadImageTag(database string) (string, error) {
var db model.Database
if err := global.DB.Model(&model.Database{}).Where("name = ?", database).First(&db).Error; err != nil {
return "", fmt.Errorf("load database %s info failed, err: %v", database, err)
}
client, err := docker.NewDockerClient()
if err != nil {
return "", err
return "", fmt.Errorf("create docker client failed, err: %v", err)
}
defer client.Close()
images, err := client.ImageList(context.Background(), image.ListOptions{})
if err != nil {
return "", err
}
itemTag := ""
for _, item := range versions {
for _, image := range images {
for _, tag := range image.RepoTags {
if tag == item {
itemTag = tag
break
}
}
if len(itemTag) != 0 {
break
images, _ := client.ImageList(context.Background(), image.ListOptions{})
for _, image := range images {
for _, tag := range image.RepoTags {
if strings.HasPrefix(tag, "postgres:"+strings.TrimSuffix(db.Version, "x")) {
return tag, nil
}
}
if len(itemTag) != 0 {
break
}
}
if len(itemTag) != 0 {
return itemTag, nil
}
sort.Strings(versions)
if len(versions) != 0 {
itemTag = versions[len(versions)-1]
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
if _, err := client.ImagePull(ctx, itemTag, image.PullOptions{}); err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return itemTag, buserr.WithName("ErrPgImagePull", itemTag)
}
global.LOG.Errorf("image %s pull failed, err: %v", itemTag, err)
return itemTag, fmt.Errorf("image %s pull failed, err: %v", itemTag, err)
}
return itemTag, nil
return "postgres:" + strings.ReplaceAll(db.Version, ".x", "-alpine"), nil
}

View file

@ -121,7 +121,7 @@ const onOpenDialog = async (
rowData: Partial<Database.DatabaseInfo> = {
name: '',
type: 'postgresql',
version: '17.x',
version: '18.x',
address: '',
port: 5432,
username: '',

View file

@ -13,6 +13,7 @@
</el-form-item>
<el-form-item :label="$t('database.version')" prop="version">
<el-radio-group v-model="dialogData.rowData!.version" @change="isOK = false">
<el-radio label="18.x" value="18.x" />
<el-radio label="17.x" value="17.x" />
<el-radio label="16.x" value="16.x" />
<el-radio label="15.x" value="15.x" />