mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 增加应用回滚功能
This commit is contained in:
parent
9f55523972
commit
d7fc670136
@ -110,10 +110,12 @@ var (
|
|||||||
Delete AppOperate = "delete"
|
Delete AppOperate = "delete"
|
||||||
Sync AppOperate = "sync"
|
Sync AppOperate = "sync"
|
||||||
Backup AppOperate = "backup"
|
Backup AppOperate = "backup"
|
||||||
|
Restore AppOperate = "restore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppInstallOperate struct {
|
type AppInstallOperate struct {
|
||||||
InstallId uint `json:"installId" validate:"required"`
|
InstallId uint `json:"installId" validate:"required"`
|
||||||
|
BackupId uint `json:"backupId"`
|
||||||
Operate AppOperate `json:"operate" validate:"required"`
|
Operate AppOperate `json:"operate" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ type AppInstallBackup struct {
|
|||||||
BaseModel
|
BaseModel
|
||||||
Name string `gorm:"type:varchar(64);not null" json:"name"`
|
Name string `gorm:"type:varchar(64);not null" json:"name"`
|
||||||
Path string `gorm:"type:varchar(64);not null" json:"path"`
|
Path string `gorm:"type:varchar(64);not null" json:"path"`
|
||||||
|
Param string `json:"param" gorm:"type:longtext;"`
|
||||||
AppDetailId uint `gorm:"type:varchar(64);not null" json:"app_detail_id"`
|
AppDetailId uint `gorm:"type:varchar(64);not null" json:"app_detail_id"`
|
||||||
AppInstallId uint `gorm:"type:integer;not null" json:"app_install_id"`
|
AppInstallId uint `gorm:"type:integer;not null" json:"app_install_id"`
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ func (a AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) e
|
|||||||
return db.Create(&install).Error
|
return db.Create(&install).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppInstallRepo) Save(install model.AppInstall) error {
|
func (a AppInstallRepo) Save(install *model.AppInstall) error {
|
||||||
return getDb().Save(&install).Error
|
return getDb().Save(&install).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,13 @@ func (a AppInstallBackupRepo) GetBy(opts ...DBOption) ([]model.AppInstallBackup,
|
|||||||
return backups, nil
|
return backups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AppInstallBackupRepo) GetFirst(opts ...DBOption) (model.AppInstallBackup, error) {
|
||||||
|
var backup model.AppInstallBackup
|
||||||
|
db := getDb(opts...).Model(&model.AppInstallBackup{})
|
||||||
|
err := db.First(&backup).Error
|
||||||
|
return backup, err
|
||||||
|
}
|
||||||
|
|
||||||
func (a AppInstallBackupRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstallBackup, error) {
|
func (a AppInstallBackupRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstallBackup, error) {
|
||||||
var backups []model.AppInstallBackup
|
var backups []model.AppInstallBackup
|
||||||
db := getDb(opts...).Model(&model.AppInstallBackup{})
|
db := getDb(opts...).Model(&model.AppInstallBackup{})
|
||||||
|
@ -70,6 +70,9 @@ func (c *CommonRepo) WithIdsNotIn(ids []uint) DBOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTx(ctx context.Context, opts ...DBOption) *gorm.DB {
|
func getTx(ctx context.Context, opts ...DBOption) *gorm.DB {
|
||||||
|
if ctx == nil {
|
||||||
|
return getDb()
|
||||||
|
}
|
||||||
tx := ctx.Value("db").(*gorm.DB)
|
tx := ctx.Value("db").(*gorm.DB)
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
tx = opt(tx)
|
tx = opt(tx)
|
||||||
|
@ -208,14 +208,21 @@ func (a AppService) OperateInstall(req dto.AppInstallOperate) error {
|
|||||||
tx, ctx := getTxAndContext()
|
tx, ctx := getTxAndContext()
|
||||||
if err := backupInstall(ctx, install); err != nil {
|
if err := backupInstall(ctx, install); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
return nil
|
return nil
|
||||||
|
case dto.Restore:
|
||||||
|
installBackup, err := appInstallBackupRepo.GetFirst(commonRepo.WithByID(req.BackupId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return restoreInstall(install, installBackup)
|
||||||
default:
|
default:
|
||||||
return errors.New("operate not support")
|
return errors.New("operate not support")
|
||||||
}
|
}
|
||||||
|
|
||||||
return appInstallRepo.Save(install)
|
return appInstallRepo.Save(&install)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppService) Install(name string, appDetailId uint, params map[string]interface{}) error {
|
func (a AppService) Install(name string, appDetailId uint, params map[string]interface{}) error {
|
||||||
@ -328,12 +335,19 @@ func (a AppService) DeleteBackup(req dto.AppBackupDeleteRequest) error {
|
|||||||
}
|
}
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
|
|
||||||
|
var errStr strings.Builder
|
||||||
for _, backup := range backups {
|
for _, backup := range backups {
|
||||||
dst := path.Join(backup.Path, backup.Name)
|
dst := path.Join(backup.Path, backup.Name)
|
||||||
if err := fileOp.DeleteFile(dst); err != nil {
|
if err := fileOp.DeleteFile(dst); err != nil {
|
||||||
|
errStr.WriteString(err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
appInstallBackupRepo.Delete(commonRepo.WithIdsIn(req.Ids))
|
if err := appInstallBackupRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil {
|
||||||
|
errStr.WriteString(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errStr.String() != "" {
|
||||||
|
return errors.New(errStr.String())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -411,11 +425,11 @@ func (a AppService) SyncInstalled(installId uint) error {
|
|||||||
if containerCount == 0 {
|
if containerCount == 0 {
|
||||||
appInstall.Status = constant.Error
|
appInstall.Status = constant.Error
|
||||||
appInstall.Message = "container is not found"
|
appInstall.Message = "container is not found"
|
||||||
return appInstallRepo.Save(appInstall)
|
return appInstallRepo.Save(&appInstall)
|
||||||
}
|
}
|
||||||
if errCount == 0 && notFoundCount == 0 {
|
if errCount == 0 && notFoundCount == 0 {
|
||||||
appInstall.Status = constant.Running
|
appInstall.Status = constant.Running
|
||||||
return appInstallRepo.Save(appInstall)
|
return appInstallRepo.Save(&appInstall)
|
||||||
}
|
}
|
||||||
if errCount == normalCount {
|
if errCount == normalCount {
|
||||||
appInstall.Status = constant.Error
|
appInstall.Status = constant.Error
|
||||||
@ -443,7 +457,7 @@ func (a AppService) SyncInstalled(installId uint) error {
|
|||||||
errMsg.Write([]byte("\n"))
|
errMsg.Write([]byte("\n"))
|
||||||
}
|
}
|
||||||
appInstall.Message = errMsg.String()
|
appInstall.Message = errMsg.String()
|
||||||
return appInstallRepo.Save(appInstall)
|
return appInstallRepo.Save(&appInstall)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppService) SyncAppList() error {
|
func (a AppService) SyncAppList() error {
|
||||||
@ -513,7 +527,7 @@ func (a AppService) SyncAppList() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
detail.DockerCompose = string(dockerComposeStr)
|
detail.DockerCompose = string(dockerComposeStr)
|
||||||
paramStr, err := os.ReadFile(path.Join(detailPath, "params.json"))
|
paramStr, err := os.ReadFile(path.Join(detailPath, "config.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error())
|
global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,9 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/utils/files"
|
"github.com/1Panel-dev/1Panel/utils/files"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -175,6 +177,7 @@ func backupInstall(ctx context.Context, install model.AppInstall) error {
|
|||||||
backup.Path = backupDir
|
backup.Path = backupDir
|
||||||
backup.AppInstallId = install.ID
|
backup.AppInstallId = install.ID
|
||||||
backup.AppDetailId = install.AppDetailId
|
backup.AppDetailId = install.AppDetailId
|
||||||
|
backup.Param = install.Param
|
||||||
|
|
||||||
if err := appInstallBackupRepo.Create(ctx, backup); err != nil {
|
if err := appInstallBackupRepo.Create(ctx, backup); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -182,6 +185,76 @@ func backupInstall(ctx context.Context, install model.AppInstall) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restoreInstall(install model.AppInstall, backup model.AppInstallBackup) error {
|
||||||
|
if _, err := compose.Down(install.GetComposePath()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
installKeyDir := path.Join(constant.AppInstallDir, install.App.Key)
|
||||||
|
installDir := path.Join(installKeyDir, install.Name)
|
||||||
|
backupFile := path.Join(backup.Path, backup.Name)
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
if !fileOp.Stat(backupFile) {
|
||||||
|
return errors.New(fmt.Sprintf("%s file is not exist", backup.Name))
|
||||||
|
}
|
||||||
|
backDir := installDir + "_back"
|
||||||
|
if fileOp.Stat(backDir) {
|
||||||
|
if err := fileOp.DeleteDir(backDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := fileOp.Rename(installDir, backDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fileOp.Decompress(backupFile, installKeyDir, files.TarGz); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
composeContent, err := os.ReadFile(install.GetComposePath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
install.DockerCompose = string(composeContent)
|
||||||
|
envContent, err := os.ReadFile(path.Join(installDir, ".env"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
install.Env = string(envContent)
|
||||||
|
envMaps, err := godotenv.Unmarshal(string(envContent))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
install.HttpPort = 0
|
||||||
|
httpPort, ok := envMaps["PANEL_APP_PORT_HTTP"]
|
||||||
|
if ok {
|
||||||
|
httpPortN, _ := strconv.Atoi(httpPort)
|
||||||
|
install.HttpPort = httpPortN
|
||||||
|
}
|
||||||
|
install.HttpsPort = 0
|
||||||
|
httpsPort, ok := envMaps["PANEL_APP_PORT_HTTPS"]
|
||||||
|
if ok {
|
||||||
|
httpsPortN, _ := strconv.Atoi(httpsPort)
|
||||||
|
install.HttpsPort = httpsPortN
|
||||||
|
}
|
||||||
|
|
||||||
|
composeMap := make(map[string]interface{})
|
||||||
|
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
servicesMap := composeMap["services"].(map[string]interface{})
|
||||||
|
for k, v := range servicesMap {
|
||||||
|
install.ServiceName = k
|
||||||
|
value := v.(map[string]interface{})
|
||||||
|
install.ContainerName = value["container_name"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
install.Param = backup.Param
|
||||||
|
_ = fileOp.DeleteDir(backDir)
|
||||||
|
if out, err := compose.Up(install.GetComposePath()); err != nil {
|
||||||
|
return handleErr(install, err, out)
|
||||||
|
}
|
||||||
|
install.Status = constant.Running
|
||||||
|
return appInstallRepo.Save(&install)
|
||||||
|
}
|
||||||
|
|
||||||
func getContainerNames(install model.AppInstall) ([]string, error) {
|
func getContainerNames(install model.AppInstall) ([]string, error) {
|
||||||
composeMap := install.DockerCompose
|
composeMap := install.DockerCompose
|
||||||
envMap := make(map[string]string)
|
envMap := make(map[string]string)
|
||||||
@ -241,6 +314,19 @@ func checkRequiredAndLimit(app model.App) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleMap(params map[string]interface{}, envParams map[string]string) {
|
||||||
|
for k, v := range params {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
envParams[k] = t
|
||||||
|
case float64:
|
||||||
|
envParams[k] = strconv.FormatFloat(t, 'f', -1, 32)
|
||||||
|
default:
|
||||||
|
envParams[k] = t.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func copyAppData(key, version, installName string, params map[string]interface{}) (err error) {
|
func copyAppData(key, version, installName string, params map[string]interface{}) (err error) {
|
||||||
resourceDir := path.Join(constant.AppResourceDir, key, version)
|
resourceDir := path.Join(constant.AppResourceDir, key, version)
|
||||||
installDir := path.Join(constant.AppInstallDir, key)
|
installDir := path.Join(constant.AppInstallDir, key)
|
||||||
@ -256,16 +342,7 @@ func copyAppData(key, version, installName string, params map[string]interface{}
|
|||||||
envPath := path.Join(appDir, ".env")
|
envPath := path.Join(appDir, ".env")
|
||||||
|
|
||||||
envParams := make(map[string]string, len(params))
|
envParams := make(map[string]string, len(params))
|
||||||
for k, v := range params {
|
handleMap(params, envParams)
|
||||||
switch t := v.(type) {
|
|
||||||
case string:
|
|
||||||
envParams[k] = t
|
|
||||||
case float64:
|
|
||||||
envParams[k] = strconv.FormatFloat(t, 'f', -1, 32)
|
|
||||||
default:
|
|
||||||
envParams[k] = t.(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = godotenv.Write(envParams, envPath); err != nil {
|
if err = godotenv.Write(envParams, envPath); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -281,10 +358,10 @@ func upApp(composeFilePath string, appInstall model.AppInstall) {
|
|||||||
appInstall.Message = err.Error()
|
appInstall.Message = err.Error()
|
||||||
}
|
}
|
||||||
appInstall.Status = constant.Error
|
appInstall.Status = constant.Error
|
||||||
_ = appInstallRepo.Save(appInstall)
|
_ = appInstallRepo.Save(&appInstall)
|
||||||
} else {
|
} else {
|
||||||
appInstall.Status = constant.Running
|
appInstall.Status = constant.Running
|
||||||
_ = appInstallRepo.Save(appInstall)
|
_ = appInstallRepo.Save(&appInstall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,6 +419,6 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
|||||||
install.Message = out
|
install.Message = out
|
||||||
reErr = errors.New(out)
|
reErr = errors.New(out)
|
||||||
}
|
}
|
||||||
_ = appInstallRepo.Save(install)
|
_ = appInstallRepo.Save(&install)
|
||||||
return reErr
|
return reErr
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@ export namespace App {
|
|||||||
export interface AppInstalledOp {
|
export interface AppInstalledOp {
|
||||||
installId: number;
|
installId: number;
|
||||||
operate: string;
|
operate: string;
|
||||||
|
backupId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppService {
|
export interface AppService {
|
||||||
|
@ -422,5 +422,8 @@ export default {
|
|||||||
backupName: 'Filename',
|
backupName: 'Filename',
|
||||||
backupPath: 'Filepath',
|
backupPath: 'Filepath',
|
||||||
backupdate: 'Backup Date',
|
backupdate: 'Backup Date',
|
||||||
|
restore: 'Restore',
|
||||||
|
restoreWarn:
|
||||||
|
'The rollback operation will restart the application and replace the data. This operation cannot be rolled back. Do you want to continue?',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -414,5 +414,7 @@ export default {
|
|||||||
backupName: '文件名称',
|
backupName: '文件名称',
|
||||||
backupPath: '文件路径',
|
backupPath: '文件路径',
|
||||||
backupdate: '备份时间',
|
backupdate: '备份时间',
|
||||||
|
restore: '回滚',
|
||||||
|
restoreWarn: '回滚操作会重启应用,并替换数据,此操作不可回滚,是否继续?',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,19 @@
|
|||||||
fix
|
fix
|
||||||
/>
|
/>
|
||||||
</ComplexTable>
|
</ComplexTable>
|
||||||
|
<el-dialog v-model="openRestorePage" :title="$t('commons.msg.operate')" width="30%" :show-close="false">
|
||||||
|
<el-alert :title="$t('app.restoreWarn')" type="warning" :closable="false" show-icon />
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="openRestorePage = false" :loading="loading">
|
||||||
|
{{ $t('commons.button.cancel') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="restore" :loading="loading">
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -42,14 +55,22 @@ const installData = ref<InstallRrops>({
|
|||||||
let open = ref(false);
|
let open = ref(false);
|
||||||
let loading = ref(false);
|
let loading = ref(false);
|
||||||
let data = ref<any>();
|
let data = ref<any>();
|
||||||
|
let openRestorePage = ref(false);
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
let req = reactive({
|
||||||
|
installId: installData.value.appInstallId,
|
||||||
|
operate: 'restore',
|
||||||
|
backupId: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const em = defineEmits(['close']);
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
open.value = false;
|
open.value = false;
|
||||||
|
em('close', open);
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptParams = (props: InstallRrops) => {
|
const acceptParams = (props: InstallRrops) => {
|
||||||
@ -86,6 +107,26 @@ const backup = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openRestore = (backupId: number) => {
|
||||||
|
openRestorePage.value = true;
|
||||||
|
req.backupId = backupId;
|
||||||
|
req.operate = 'restore';
|
||||||
|
req.installId = installData.value.appInstallId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const restore = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
await InstalledOp(req)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
openRestorePage.value = false;
|
||||||
|
search();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const deleteBackup = async (ids: number[]) => {
|
const deleteBackup = async (ids: number[]) => {
|
||||||
const req = {
|
const req = {
|
||||||
ids: ids,
|
ids: ids,
|
||||||
@ -101,6 +142,12 @@ const buttons = [
|
|||||||
deleteBackup([row.id]);
|
deleteBackup([row.id]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: i18n.global.t('app.restore'),
|
||||||
|
click: (row: any) => {
|
||||||
|
openRestore(row.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
@ -59,7 +59,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<Backups ref="backupRef"></Backups>
|
<Backups ref="backupRef" @close="search"></Backups>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user