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
c3274af4cd
commit
000c475626
@ -23,7 +23,7 @@ func (a AppInstallRepo) Create(install *model.AppInstall) error {
|
||||
}
|
||||
|
||||
func (a AppInstallRepo) Save(install model.AppInstall) error {
|
||||
db := global.DB.Model(&model.AppInstall{})
|
||||
db := global.DB
|
||||
return db.Save(&install).Error
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/utils/files"
|
||||
"github.com/joho/godotenv"
|
||||
"golang.org/x/net/context"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
@ -165,34 +166,42 @@ func (a AppService) Operate(req dto.AppInstallOperate) error {
|
||||
if err != nil {
|
||||
return handleErr(install, err, out)
|
||||
}
|
||||
install.Status = constant.Running
|
||||
case dto.Down:
|
||||
out, err := compose.Down(dockerComposePath)
|
||||
if err != nil {
|
||||
return handleErr(install, err, out)
|
||||
}
|
||||
install.Status = constant.Stopped
|
||||
case dto.Restart:
|
||||
out, err := compose.Restart(dockerComposePath)
|
||||
if err != nil {
|
||||
return handleErr(install, err, out)
|
||||
}
|
||||
install.Status = constant.Running
|
||||
case dto.Delete:
|
||||
op := files.NewFileOp()
|
||||
appDir := path.Join(global.CONF.System.AppDir, install.App.Key, install.ContainerName)
|
||||
dir, _ := os.Stat(appDir)
|
||||
if dir == nil {
|
||||
_ = appInstallRepo.Delete(commonRepo.WithByID(install.ID))
|
||||
break
|
||||
return appInstallRepo.Delete(commonRepo.WithByID(install.ID))
|
||||
}
|
||||
_ = op.DeleteDir(appDir)
|
||||
out, err := compose.Rmf(dockerComposePath)
|
||||
out, err := compose.Down(dockerComposePath)
|
||||
if err != nil {
|
||||
return handleErr(install, err, out)
|
||||
}
|
||||
out, err = compose.Rmf(dockerComposePath)
|
||||
if err != nil {
|
||||
return handleErr(install, err, out)
|
||||
}
|
||||
_ = op.DeleteDir(appDir)
|
||||
_ = appInstallRepo.Delete(commonRepo.WithByID(install.ID))
|
||||
return nil
|
||||
default:
|
||||
return errors.New("operate not support")
|
||||
}
|
||||
return nil
|
||||
|
||||
return appInstallRepo.Save(install)
|
||||
}
|
||||
|
||||
func handleErr(install model.AppInstall, err error, out string) error {
|
||||
@ -207,6 +216,15 @@ func handleErr(install model.AppInstall, err error, out string) error {
|
||||
}
|
||||
|
||||
func (a AppService) Install(name string, appDetailId uint, params map[string]interface{}) error {
|
||||
|
||||
port, ok := params["PORT"]
|
||||
if ok {
|
||||
port := int(math.Floor(port.(float64)))
|
||||
if common.ScanPort(string(port)) {
|
||||
return errors.New("port is in used")
|
||||
}
|
||||
}
|
||||
|
||||
appDetail, err := appDetailRepo.GetAppDetail(commonRepo.WithByID(appDetailId))
|
||||
if err != nil {
|
||||
return err
|
||||
@ -229,11 +247,11 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
|
||||
}
|
||||
resourceDir := path.Join(global.CONF.System.ResourceDir, "apps", app.Key, appDetail.Version)
|
||||
installDir := path.Join(global.CONF.System.AppDir, app.Key)
|
||||
installAppDir := path.Join(installDir, appDetail.Version)
|
||||
op := files.NewFileOp()
|
||||
if err := op.CopyDir(resourceDir, installDir); err != nil {
|
||||
if err := op.Copy(resourceDir, installAppDir); err != nil {
|
||||
return err
|
||||
}
|
||||
installAppDir := path.Join(installDir, appDetail.Version)
|
||||
containerNameDir := path.Join(installDir, containerName)
|
||||
if err := op.Rename(installAppDir, containerNameDir); err != nil {
|
||||
return err
|
||||
@ -279,6 +297,11 @@ func upApp(composeFilePath string, appInstall model.AppInstall) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a AppService) SyncInstalled() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AppService) Sync() error {
|
||||
//TODO 从 oss 拉取最新列表
|
||||
var appConfig model.AppConfig
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
mathRand "math/rand"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -60,3 +61,13 @@ func RandStr(n int) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func ScanPort(port string) bool {
|
||||
|
||||
ln, err := net.Listen("tcp", ":"+port)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
defer ln.Close()
|
||||
return false
|
||||
}
|
||||
|
@ -5,35 +5,23 @@ import "os/exec"
|
||||
func Up(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "up", "-d")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return string(stdout), err
|
||||
}
|
||||
return string(stdout), nil
|
||||
return string(stdout), err
|
||||
}
|
||||
|
||||
func Down(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "down")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(stdout), nil
|
||||
return string(stdout), err
|
||||
}
|
||||
|
||||
func Restart(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "restart")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(stdout), nil
|
||||
return string(stdout), err
|
||||
}
|
||||
|
||||
func Rmf(filePath string) (string, error) {
|
||||
cmd := exec.Command("docker-compose", "-f", filePath, "rm", "-f")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(stdout), nil
|
||||
return string(stdout), err
|
||||
}
|
||||
|
@ -207,8 +207,8 @@ func (f FileOp) CopyDir(src, dst string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dstDir := filepath.Join(dst, srcInfo.Name())
|
||||
if err := f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
|
||||
//dstDir := filepath.Join(dst, srcInfo.Name())
|
||||
if err := f.Fs.MkdirAll(dst, srcInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -221,7 +221,7 @@ func (f FileOp) CopyDir(src, dst string) error {
|
||||
|
||||
for _, obj := range obs {
|
||||
fSrc := filepath.Join(src, obj.Name())
|
||||
fDst := filepath.Join(dstDir, obj.Name())
|
||||
fDst := filepath.Join(dst, obj.Name())
|
||||
|
||||
if obj.IsDir() {
|
||||
err = f.CopyDir(fSrc, fDst)
|
||||
|
@ -48,7 +48,7 @@ let rowName = ref('');
|
||||
let data = ref();
|
||||
let loading = ref(false);
|
||||
let paths = ref<string[]>([]);
|
||||
let req = reactive({ path: '/', expand: true, page: 1, pageSize: 20 });
|
||||
let req = reactive({ path: '/', expand: true, page: 1, pageSize: 300 });
|
||||
|
||||
const props = defineProps({
|
||||
path: {
|
||||
|
@ -415,5 +415,7 @@ export default {
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
delete: 'Delete',
|
||||
deleteWarn:
|
||||
'Delete the operation data and delete the operation data. This operation cannot be rolled back. Do you want to continue?',
|
||||
},
|
||||
};
|
||||
|
@ -407,5 +407,6 @@ export default {
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
delete: '删除',
|
||||
deleteWarn: '删除操作会把数据一并删除,此操作不可回滚,是否继续?',
|
||||
},
|
||||
};
|
||||
|
@ -15,6 +15,28 @@ const appStoreRouter = {
|
||||
name: 'App',
|
||||
component: () => import('@/views/app-store/index.vue'),
|
||||
meta: {},
|
||||
children: [
|
||||
{
|
||||
path: 'all',
|
||||
name: 'AppAll',
|
||||
component: () => import('@/views/app-store/apps/index.vue'),
|
||||
props: true,
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/apps',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'installed',
|
||||
name: 'AppInstalled',
|
||||
component: () => import('@/views/app-store/installed/index.vue'),
|
||||
props: true,
|
||||
hidden: true,
|
||||
meta: {
|
||||
activeMenu: '/apps',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/apps/detail/:id',
|
||||
|
@ -6,11 +6,8 @@
|
||||
</el-form-item>
|
||||
<div v-for="(f, index) in installData.params?.formFields" :key="index">
|
||||
<el-form-item :label="f.labelZh" :prop="f.envKey">
|
||||
<el-input
|
||||
v-model="form[f.envKey]"
|
||||
v-if="f.type == 'text' || f.type == 'number'"
|
||||
:type="f.type"
|
||||
></el-input>
|
||||
<el-input v-model="form[f.envKey]" v-if="f.type == 'text'" :type="f.type"></el-input>
|
||||
<el-input v-model.number="form[f.envKey]" v-if="f.type == 'number'" :type="f.type"></el-input>
|
||||
<el-input
|
||||
v-model="form[f.envKey]"
|
||||
v-if="f.type == 'password'"
|
||||
@ -19,9 +16,6 @@
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<!-- <el-form-item :label="$t('app.description')">
|
||||
<el-input v-model="req.name"></el-input>
|
||||
</el-form-item> -->
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@ -40,6 +34,9 @@ import { InstallApp } from '@/api/modules/app';
|
||||
import { Rules } from '@/global/form-rues';
|
||||
import { FormInstance, FormRules } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
interface InstallRrops {
|
||||
appDetailId: number;
|
||||
params?: App.AppParams;
|
||||
@ -61,13 +58,11 @@ const req = reactive({
|
||||
params: {},
|
||||
name: '',
|
||||
});
|
||||
const em = defineEmits(['close']);
|
||||
const handleClose = () => {
|
||||
if (paramForm.value) {
|
||||
paramForm.value.resetFields();
|
||||
}
|
||||
open.value = false;
|
||||
em('close', open);
|
||||
};
|
||||
|
||||
const acceptParams = (props: InstallRrops): void => {
|
||||
@ -95,6 +90,7 @@ const submit = async (formEl: FormInstance | undefined) => {
|
||||
req.name = form['NAME'];
|
||||
InstallApp(req).then(() => {
|
||||
handleClose();
|
||||
router.push({ path: '/apps/installed' });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -1,40 +1,54 @@
|
||||
<template>
|
||||
<LayoutContent>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<div style="margin-bottom: 10px">
|
||||
<el-radio-group v-model="activeName">
|
||||
<el-radio-button label="all">
|
||||
<el-radio-button label="all" @click="routerTo('/apps/all')">
|
||||
{{ $t('app.all') }}
|
||||
</el-radio-button>
|
||||
<el-radio-button label="installed">
|
||||
<el-radio-button label="installed" @click="routerTo('/apps/installed')">
|
||||
{{ $t('app.installed') }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div style="float: right">
|
||||
<el-button @click="sync">{{ $t('app.sync') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<Apps v-if="activeName === 'all'"></Apps>
|
||||
<Installed v-if="activeName === 'installed'"></Installed>
|
||||
</LayoutContent>
|
||||
<LayoutContent>
|
||||
<router-view></router-view>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import { ref } from 'vue';
|
||||
import { SyncApp } from '@/api/modules/app';
|
||||
import Apps from './apps/index.vue';
|
||||
import Installed from './installed/index.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const activeName = ref('all');
|
||||
|
||||
const sync = () => {
|
||||
SyncApp().then((res) => {
|
||||
console.log(res);
|
||||
});
|
||||
// const sync = () => {
|
||||
// SyncApp().then((res) => {
|
||||
// console.log(res);
|
||||
// });
|
||||
// };
|
||||
|
||||
const routerTo = (path: string) => {
|
||||
router.push({ path: path });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const path = router.currentRoute.value.path;
|
||||
if (path === '/apps/all') {
|
||||
activeName.value = 'all';
|
||||
}
|
||||
if (path === '/apps/installed') {
|
||||
activeName.value = 'installed';
|
||||
}
|
||||
if (path === '/apps') {
|
||||
routerTo('/apps/all');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -13,7 +13,7 @@
|
||||
<template #default="{ row }">
|
||||
<el-popover
|
||||
v-if="row.status === 'Error'"
|
||||
placement="top-start"
|
||||
placement="bottom"
|
||||
:width="400"
|
||||
trigger="hover"
|
||||
:content="row.message"
|
||||
@ -31,8 +31,24 @@
|
||||
:formatter="dateFromat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" fix />
|
||||
<fu-table-operations
|
||||
width="200px"
|
||||
:ellipsis="10"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
<el-dialog v-model="open" :title="$t('commons.msg.operate')" :before-close="handleClose" width="30%">
|
||||
<el-alert :title="getMsg(operateReq.operate)" type="warning" :closable="false" show-icon />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="operate">{{ $t('commons.button.confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -41,7 +57,7 @@ import { onMounted, reactive, ref } from 'vue';
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { dateFromat } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
let data = ref<any>();
|
||||
let loading = ref(false);
|
||||
@ -50,6 +66,11 @@ const paginationConfig = reactive({
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
});
|
||||
let open = ref(false);
|
||||
let operateReq = reactive({
|
||||
installId: 0,
|
||||
operate: '',
|
||||
});
|
||||
|
||||
const search = () => {
|
||||
const req = {
|
||||
@ -63,53 +84,72 @@ const search = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const operate = async (row: any, op: string) => {
|
||||
const req = {
|
||||
installId: row.id,
|
||||
operate: op,
|
||||
};
|
||||
const openOperate = (row: any, op: string) => {
|
||||
operateReq.installId = row.id;
|
||||
operateReq.operate = op;
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
ElMessageBox.confirm(i18n.global.t(`${'app.' + op}`) + '?', i18n.global.t('commons.msg.operate'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'warning',
|
||||
draggable: true,
|
||||
}).then(async () => {
|
||||
loading.value = true;
|
||||
InstalledOp(req)
|
||||
.then(() => {
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
const operate = async () => {
|
||||
open.value = false;
|
||||
loading.value = true;
|
||||
await InstalledOp(operateReq)
|
||||
.then(() => {
|
||||
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const getMsg = (op: string) => {
|
||||
let tip = '';
|
||||
switch (op) {
|
||||
case 'up':
|
||||
tip = i18n.global.t('app.up');
|
||||
break;
|
||||
case 'down':
|
||||
tip = i18n.global.t('app.down');
|
||||
break;
|
||||
case 'restart':
|
||||
tip = i18n.global.t('app.restart');
|
||||
break;
|
||||
case 'delete':
|
||||
tip = i18n.global.t('app.deleteWarn');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return tip;
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('app.restart'),
|
||||
click: (row: any) => {
|
||||
operate(row, 'restart');
|
||||
openOperate(row, 'restart');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('app.up'),
|
||||
click: (row: any) => {
|
||||
operate(row, 'up');
|
||||
openOperate(row, 'up');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('app.down'),
|
||||
click: (row: any) => {
|
||||
operate(row, 'down');
|
||||
openOperate(row, 'down');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('app.delete'),
|
||||
click: (row: any) => {
|
||||
operate(row, 'delete');
|
||||
openOperate(row, 'delete');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -37,14 +37,15 @@ const handleClose = () => {
|
||||
em('close', open);
|
||||
};
|
||||
|
||||
const closeSocket = () => {
|
||||
processSocket && processSocket.close();
|
||||
};
|
||||
|
||||
const isWsOpen = () => {
|
||||
const readyState = processSocket && processSocket.readyState;
|
||||
return readyState === 1;
|
||||
};
|
||||
const closeSocket = () => {
|
||||
if (isWsOpen()) {
|
||||
processSocket && processSocket.close();
|
||||
}
|
||||
};
|
||||
|
||||
const onOpenProcess = () => {};
|
||||
const onMessage = (message: any) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user