mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-09 00:24:55 +08:00
feat: PG remote databases version 18.x support backup and restore (#11048)
Refs #10917
This commit is contained in:
parent
a853d8c869
commit
7c33dd9026
16 changed files with 51 additions and 66 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@ ErrDatabaseIsExist: '現在のデータベースは既に存在します。再
|
|||
ErrExecTimeOut: 'SQL 実行がタイムアウトしました。データベースを確認してください'
|
||||
ErrRemoteExist: 'この名前のリモート データベースは既に存在します。変更してもう一度お試しください'
|
||||
ErrLocalExist: '名前はローカル データベースに既に存在します。変更してもう一度お試しください'
|
||||
RemoteBackup: "リモートデータベースをバックアップするには、まずイメージ {{ .name }} を使用してローカルコンテナデータベースサービスを起動する必要があります。しばらくお待ちください..."
|
||||
RemoteRecover: "リモートデータベースを復元するには、まずイメージ {{ .name }} を使用してローカルコンテナデータベースサービスを起動する必要があります。しばらくお待ちください..."
|
||||
|
||||
#redis
|
||||
ErrTypeOfRedis: 'リカバリ ファイルの種類が現在の永続化方法と一致しません。変更して再試行してください'
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ ErrDatabaseIsExist: '현재 데이터베이스가 이미 존재합니다. 다시
|
|||
ErrExecTimeOut: 'SQL 실행 시간이 초과되었습니다. 데이터베이스를 확인하십시오.'
|
||||
ErrRemoteExist: '이 이름을 가진 원격 데이터베이스가 이미 존재합니다. 수정하고 다시 시도하세요'
|
||||
ErrLocalExist: '이름이 로컬 데이터베이스에 이미 존재합니다. 이름을 수정하고 다시 시도하세요'
|
||||
RemoteBackup: "원격 데이터베이스를 백업하려면 먼저 이미지 {{ .name }}을(를) 사용하여 로컬 컨테이너 데이터베이스 서비스를 시작해야 합니다. 잠시만 기다려 주세요..."
|
||||
RemoteRecover: "원격 데이터베이스를 복원하려면 먼저 이미지 {{ .name }}을(를) 사용하여 로컬 컨테이너 데이터베이스 서비스를 시작해야 합니다. 잠시만 기다려 주세요..."
|
||||
|
||||
#레디스
|
||||
ErrTypeOfRedis: '복구 파일 유형이 현재 지속성 방법과 일치하지 않습니다. 수정하고 다시 시도하세요'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ ErrDatabaseIsExist: 'Текущая база данных уже существ
|
|||
ErrExecTimeOut: 'Время выполнения SQL истекло, проверьте базу данных'
|
||||
ErrRemoteExist: 'Удаленная база данных с таким именем уже существует. Измените его и повторите попытку'
|
||||
ErrLocalExist: 'Имя уже существует в локальной базе данных, измените его и повторите попытку'
|
||||
RemoteBackup: "Для резервного копирования удаленной базы данных необходимо сначала запустить службу базы данных локального контейнера с помощью образа {{ .name }}, пожалуйста, подождите..."
|
||||
RemoteRecover: "Для восстановления удаленной базы данных необходимо сначала запустить службу базы данных локального контейнера с помощью образа {{ .name }}, пожалуйста, подождите..."
|
||||
|
||||
#редис
|
||||
ErrTypeOfRedis: 'Тип файла восстановления не соответствует текущему методу сохранения. Измените его и повторите попытку'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@ ErrDatabaseIsExist: '目前資料庫已存在,請重新輸入'
|
|||
ErrExecTimeOut: 'SQL 執行逾時,請檢查資料庫'
|
||||
ErrRemoteExist: '遠端資料庫已存在該名稱,請修改後重試'
|
||||
ErrLocalExist: '本機資料庫已存在該名稱,請修改後重試'
|
||||
RemoteBackup: "備份遠端資料庫需要先使用映像 {{ .name }} 啟動本機容器資料庫服務,請稍候..."
|
||||
RemoteRecover: "恢復遠端資料庫需要先使用映像 {{ .name }} 啟動本機容器資料庫服務,請稍候..."
|
||||
|
||||
#redis
|
||||
ErrTypeOfRedis: '復原檔案類型與目前持久化方式不符,請修改後重試'
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ ErrDatabaseIsExist: "当前数据库已存在,请重新输入"
|
|||
ErrExecTimeOut: "SQL 执行超时,请检查数据库"
|
||||
ErrRemoteExist: "远程数据库已存在该名称,请修改后重试"
|
||||
ErrLocalExist: "本地数据库已存在该名称,请修改后重试"
|
||||
RemoteBackup: "备份远程数据库需要先使用镜像 {{ .name }} 启动本地容器数据库服务,请稍候..."
|
||||
RemoteRecover: "恢复远程数据库需要先使用镜像 {{ .name }} 启动本地容器数据库服务,请稍候..."
|
||||
|
||||
#redis
|
||||
ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后重试"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ const onOpenDialog = async (
|
|||
rowData: Partial<Database.DatabaseInfo> = {
|
||||
name: '',
|
||||
type: 'postgresql',
|
||||
version: '17.x',
|
||||
version: '18.x',
|
||||
address: '',
|
||||
port: 5432,
|
||||
username: '',
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue