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

feat: 增加端口检测 优化删除逻辑

This commit is contained in:
zhengkunwang223 2022-09-27 16:57:23 +08:00 committed by zhengkunwang223
parent c3274af4cd
commit 000c475626
13 changed files with 185 additions and 87 deletions

View File

@ -23,7 +23,7 @@ func (a AppInstallRepo) Create(install *model.AppInstall) error {
} }
func (a AppInstallRepo) Save(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 return db.Save(&install).Error
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/1Panel-dev/1Panel/utils/files" "github.com/1Panel-dev/1Panel/utils/files"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"golang.org/x/net/context" "golang.org/x/net/context"
"math"
"os" "os"
"path" "path"
"reflect" "reflect"
@ -165,34 +166,42 @@ func (a AppService) Operate(req dto.AppInstallOperate) error {
if err != nil { if err != nil {
return handleErr(install, err, out) return handleErr(install, err, out)
} }
install.Status = constant.Running
case dto.Down: case dto.Down:
out, err := compose.Down(dockerComposePath) out, err := compose.Down(dockerComposePath)
if err != nil { if err != nil {
return handleErr(install, err, out) return handleErr(install, err, out)
} }
install.Status = constant.Stopped
case dto.Restart: case dto.Restart:
out, err := compose.Restart(dockerComposePath) out, err := compose.Restart(dockerComposePath)
if err != nil { if err != nil {
return handleErr(install, err, out) return handleErr(install, err, out)
} }
install.Status = constant.Running
case dto.Delete: case dto.Delete:
op := files.NewFileOp() op := files.NewFileOp()
appDir := path.Join(global.CONF.System.AppDir, install.App.Key, install.ContainerName) appDir := path.Join(global.CONF.System.AppDir, install.App.Key, install.ContainerName)
dir, _ := os.Stat(appDir) dir, _ := os.Stat(appDir)
if dir == nil { if dir == nil {
_ = appInstallRepo.Delete(commonRepo.WithByID(install.ID)) return appInstallRepo.Delete(commonRepo.WithByID(install.ID))
break
} }
_ = op.DeleteDir(appDir) out, err := compose.Down(dockerComposePath)
out, err := compose.Rmf(dockerComposePath)
if err != nil { if err != nil {
return handleErr(install, err, out) 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)) _ = appInstallRepo.Delete(commonRepo.WithByID(install.ID))
return nil
default: default:
return errors.New("operate not support") return errors.New("operate not support")
} }
return nil
return appInstallRepo.Save(install)
} }
func handleErr(install model.AppInstall, err error, out string) error { 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 { 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)) appDetail, err := appDetailRepo.GetAppDetail(commonRepo.WithByID(appDetailId))
if err != nil { if err != nil {
return err 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) resourceDir := path.Join(global.CONF.System.ResourceDir, "apps", app.Key, appDetail.Version)
installDir := path.Join(global.CONF.System.AppDir, app.Key) installDir := path.Join(global.CONF.System.AppDir, app.Key)
installAppDir := path.Join(installDir, appDetail.Version)
op := files.NewFileOp() op := files.NewFileOp()
if err := op.CopyDir(resourceDir, installDir); err != nil { if err := op.Copy(resourceDir, installAppDir); err != nil {
return err return err
} }
installAppDir := path.Join(installDir, appDetail.Version)
containerNameDir := path.Join(installDir, containerName) containerNameDir := path.Join(installDir, containerName)
if err := op.Rename(installAppDir, containerNameDir); err != nil { if err := op.Rename(installAppDir, containerNameDir); err != nil {
return err 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 { func (a AppService) Sync() error {
//TODO 从 oss 拉取最新列表 //TODO 从 oss 拉取最新列表
var appConfig model.AppConfig var appConfig model.AppConfig

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
mathRand "math/rand" mathRand "math/rand"
"net"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -60,3 +61,13 @@ func RandStr(n int) string {
} }
return string(b) return string(b)
} }
func ScanPort(port string) bool {
ln, err := net.Listen("tcp", ":"+port)
if err != nil {
return true
}
defer ln.Close()
return false
}

View File

@ -5,35 +5,23 @@ import "os/exec"
func Up(filePath string) (string, error) { func Up(filePath string) (string, error) {
cmd := exec.Command("docker-compose", "-f", filePath, "up", "-d") cmd := exec.Command("docker-compose", "-f", filePath, "up", "-d")
stdout, err := cmd.CombinedOutput() stdout, err := cmd.CombinedOutput()
if err != nil { return string(stdout), err
return string(stdout), err
}
return string(stdout), nil
} }
func Down(filePath string) (string, error) { func Down(filePath string) (string, error) {
cmd := exec.Command("docker-compose", "-f", filePath, "down") cmd := exec.Command("docker-compose", "-f", filePath, "down")
stdout, err := cmd.CombinedOutput() stdout, err := cmd.CombinedOutput()
if err != nil { return string(stdout), err
return "", err
}
return string(stdout), nil
} }
func Restart(filePath string) (string, error) { func Restart(filePath string) (string, error) {
cmd := exec.Command("docker-compose", "-f", filePath, "restart") cmd := exec.Command("docker-compose", "-f", filePath, "restart")
stdout, err := cmd.CombinedOutput() stdout, err := cmd.CombinedOutput()
if err != nil { return string(stdout), err
return "", err
}
return string(stdout), nil
} }
func Rmf(filePath string) (string, error) { func Rmf(filePath string) (string, error) {
cmd := exec.Command("docker-compose", "-f", filePath, "rm", "-f") cmd := exec.Command("docker-compose", "-f", filePath, "rm", "-f")
stdout, err := cmd.CombinedOutput() stdout, err := cmd.CombinedOutput()
if err != nil { return string(stdout), err
return "", err
}
return string(stdout), nil
} }

View File

@ -207,8 +207,8 @@ func (f FileOp) CopyDir(src, dst string) error {
if err != nil { if err != nil {
return err return err
} }
dstDir := filepath.Join(dst, srcInfo.Name()) //dstDir := filepath.Join(dst, srcInfo.Name())
if err := f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil { if err := f.Fs.MkdirAll(dst, srcInfo.Mode()); err != nil {
return err return err
} }
@ -221,7 +221,7 @@ func (f FileOp) CopyDir(src, dst string) error {
for _, obj := range obs { for _, obj := range obs {
fSrc := filepath.Join(src, obj.Name()) fSrc := filepath.Join(src, obj.Name())
fDst := filepath.Join(dstDir, obj.Name()) fDst := filepath.Join(dst, obj.Name())
if obj.IsDir() { if obj.IsDir() {
err = f.CopyDir(fSrc, fDst) err = f.CopyDir(fSrc, fDst)

View File

@ -48,7 +48,7 @@ let rowName = ref('');
let data = ref(); let data = ref();
let loading = ref(false); let loading = ref(false);
let paths = ref<string[]>([]); 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({ const props = defineProps({
path: { path: {

View File

@ -415,5 +415,7 @@ export default {
name: 'Name', name: 'Name',
description: 'Description', description: 'Description',
delete: 'Delete', delete: 'Delete',
deleteWarn:
'Delete the operation data and delete the operation data. This operation cannot be rolled back. Do you want to continue?',
}, },
}; };

View File

@ -407,5 +407,6 @@ export default {
name: '名称', name: '名称',
description: '描述', description: '描述',
delete: '删除', delete: '删除',
deleteWarn: '删除操作会把数据一并删除,此操作不可回滚,是否继续?',
}, },
}; };

View File

@ -15,6 +15,28 @@ const appStoreRouter = {
name: 'App', name: 'App',
component: () => import('@/views/app-store/index.vue'), component: () => import('@/views/app-store/index.vue'),
meta: {}, 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', path: '/apps/detail/:id',

View File

@ -6,11 +6,8 @@
</el-form-item> </el-form-item>
<div v-for="(f, index) in installData.params?.formFields" :key="index"> <div v-for="(f, index) in installData.params?.formFields" :key="index">
<el-form-item :label="f.labelZh" :prop="f.envKey"> <el-form-item :label="f.labelZh" :prop="f.envKey">
<el-input <el-input v-model="form[f.envKey]" v-if="f.type == 'text'" :type="f.type"></el-input>
v-model="form[f.envKey]" <el-input v-model.number="form[f.envKey]" v-if="f.type == 'number'" :type="f.type"></el-input>
v-if="f.type == 'text' || f.type == 'number'"
:type="f.type"
></el-input>
<el-input <el-input
v-model="form[f.envKey]" v-model="form[f.envKey]"
v-if="f.type == 'password'" v-if="f.type == 'password'"
@ -19,9 +16,6 @@
></el-input> ></el-input>
</el-form-item> </el-form-item>
</div> </div>
<!-- <el-form-item :label="$t('app.description')">
<el-input v-model="req.name"></el-input>
</el-form-item> -->
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@ -40,6 +34,9 @@ import { InstallApp } from '@/api/modules/app';
import { Rules } from '@/global/form-rues'; import { Rules } from '@/global/form-rues';
import { FormInstance, FormRules } from 'element-plus'; import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
interface InstallRrops { interface InstallRrops {
appDetailId: number; appDetailId: number;
params?: App.AppParams; params?: App.AppParams;
@ -61,13 +58,11 @@ const req = reactive({
params: {}, params: {},
name: '', name: '',
}); });
const em = defineEmits(['close']);
const handleClose = () => { const handleClose = () => {
if (paramForm.value) { if (paramForm.value) {
paramForm.value.resetFields(); paramForm.value.resetFields();
} }
open.value = false; open.value = false;
em('close', open);
}; };
const acceptParams = (props: InstallRrops): void => { const acceptParams = (props: InstallRrops): void => {
@ -95,6 +90,7 @@ const submit = async (formEl: FormInstance | undefined) => {
req.name = form['NAME']; req.name = form['NAME'];
InstallApp(req).then(() => { InstallApp(req).then(() => {
handleClose(); handleClose();
router.push({ path: '/apps/installed' });
}); });
}); });
}; };

View File

@ -1,40 +1,54 @@
<template> <template>
<LayoutContent> <div>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="24"> <el-col :span="24">
<div style="margin-bottom: 10px"> <div style="margin-bottom: 10px">
<el-radio-group v-model="activeName"> <el-radio-group v-model="activeName">
<el-radio-button label="all"> <el-radio-button label="all" @click="routerTo('/apps/all')">
{{ $t('app.all') }} {{ $t('app.all') }}
</el-radio-button> </el-radio-button>
<el-radio-button label="installed"> <el-radio-button label="installed" @click="routerTo('/apps/installed')">
{{ $t('app.installed') }} {{ $t('app.installed') }}
</el-radio-button> </el-radio-button>
</el-radio-group> </el-radio-group>
<div style="float: right">
<el-button @click="sync">{{ $t('app.sync') }}</el-button>
</div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
<Apps v-if="activeName === 'all'"></Apps> <LayoutContent>
<Installed v-if="activeName === 'installed'"></Installed> <router-view></router-view>
</LayoutContent> </LayoutContent>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import LayoutContent from '@/layout/layout-content.vue'; import LayoutContent from '@/layout/layout-content.vue';
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import { SyncApp } from '@/api/modules/app'; import { useRouter } from 'vue-router';
import Apps from './apps/index.vue'; const router = useRouter();
import Installed from './installed/index.vue';
const activeName = ref('all'); const activeName = ref('all');
const sync = () => { // const sync = () => {
SyncApp().then((res) => { // SyncApp().then((res) => {
console.log(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> </script>
<style lang="scss"> <style lang="scss">

View File

@ -13,7 +13,7 @@
<template #default="{ row }"> <template #default="{ row }">
<el-popover <el-popover
v-if="row.status === 'Error'" v-if="row.status === 'Error'"
placement="top-start" placement="bottom"
:width="400" :width="400"
trigger="hover" trigger="hover"
:content="row.message" :content="row.message"
@ -31,8 +31,24 @@
:formatter="dateFromat" :formatter="dateFromat"
show-overflow-tooltip 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> </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> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -41,7 +57,7 @@ import { onMounted, reactive, ref } from 'vue';
import ComplexTable from '@/components/complex-table/index.vue'; import ComplexTable from '@/components/complex-table/index.vue';
import { dateFromat } from '@/utils/util'; import { dateFromat } from '@/utils/util';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage } from 'element-plus';
let data = ref<any>(); let data = ref<any>();
let loading = ref(false); let loading = ref(false);
@ -50,6 +66,11 @@ const paginationConfig = reactive({
pageSize: 20, pageSize: 20,
total: 0, total: 0,
}); });
let open = ref(false);
let operateReq = reactive({
installId: 0,
operate: '',
});
const search = () => { const search = () => {
const req = { const req = {
@ -63,53 +84,72 @@ const search = () => {
}); });
}; };
const operate = async (row: any, op: string) => { const openOperate = (row: any, op: string) => {
const req = { operateReq.installId = row.id;
installId: row.id, operateReq.operate = op;
operate: op, open.value = true;
}; };
ElMessageBox.confirm(i18n.global.t(`${'app.' + op}`) + '?', i18n.global.t('commons.msg.operate'), { const operate = async () => {
confirmButtonText: i18n.global.t('commons.button.confirm'), open.value = false;
cancelButtonText: i18n.global.t('commons.button.cancel'), loading.value = true;
type: 'warning', await InstalledOp(operateReq)
draggable: true, .then(() => {
}).then(async () => { ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
loading.value = true; search();
InstalledOp(req) })
.then(() => { .finally(() => {
ElMessage.success(i18n.global.t('commons.msg.operationSuccess')); loading.value = false;
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 = [ const buttons = [
{ {
label: i18n.global.t('app.restart'), label: i18n.global.t('app.restart'),
click: (row: any) => { click: (row: any) => {
operate(row, 'restart'); openOperate(row, 'restart');
}, },
}, },
{ {
label: i18n.global.t('app.up'), label: i18n.global.t('app.up'),
click: (row: any) => { click: (row: any) => {
operate(row, 'up'); openOperate(row, 'up');
}, },
}, },
{ {
label: i18n.global.t('app.down'), label: i18n.global.t('app.down'),
click: (row: any) => { click: (row: any) => {
operate(row, 'down'); openOperate(row, 'down');
}, },
}, },
{ {
label: i18n.global.t('app.delete'), label: i18n.global.t('app.delete'),
click: (row: any) => { click: (row: any) => {
operate(row, 'delete'); openOperate(row, 'delete');
}, },
}, },
]; ];

View File

@ -37,14 +37,15 @@ const handleClose = () => {
em('close', open); em('close', open);
}; };
const closeSocket = () => {
processSocket && processSocket.close();
};
const isWsOpen = () => { const isWsOpen = () => {
const readyState = processSocket && processSocket.readyState; const readyState = processSocket && processSocket.readyState;
return readyState === 1; return readyState === 1;
}; };
const closeSocket = () => {
if (isWsOpen()) {
processSocket && processSocket.close();
}
};
const onOpenProcess = () => {}; const onOpenProcess = () => {};
const onMessage = (message: any) => { const onMessage = (message: any) => {