1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 16:29:17 +08:00

feat: 增加应用回滚功能

This commit is contained in:
zhengkunwang223 2022-10-13 16:46:38 +08:00 committed by zhengkunwang223
parent 9f55523972
commit d7fc670136
17 changed files with 178 additions and 21 deletions

View File

@ -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"`
} }

View File

@ -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"`
} }

View File

@ -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
} }

View File

@ -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{})

View File

@ -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)

View File

@ -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())
} }

View File

@ -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
} }

View File

@ -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 {

View File

@ -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?',
}, },
}; };

View File

@ -414,5 +414,7 @@ export default {
backupName: '文件名称', backupName: '文件名称',
backupPath: '文件路径', backupPath: '文件路径',
backupdate: '备份时间', backupdate: '备份时间',
restore: '回滚',
restoreWarn: '回滚操作会重启应用,并替换数据,此操作不可回滚,是否继续?',
}, },
}; };

View File

@ -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({

View File

@ -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>