feat: 负载均衡增加源文编辑 (#6153)

pull/6169/head
zhengkunwang 3 months ago committed by GitHub
parent bf82fe743c
commit 7fb42d7b47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -917,3 +917,43 @@ func (b *BaseApi) DeleteLoadBalance(c *gin.Context) {
}
helper.SuccessWithOutData(c)
}
// @Tags Website
// @Summary Update website upstream
// @Description 更新网站 upstream
// @Accept json
// @Param request body request.WebsiteLBUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/lbs/update [post]
func (b *BaseApi) UpdateLoadBalance(c *gin.Context) {
var req request.WebsiteLBUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteService.UpdateLoadBalance(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags Website
// @Summary Update website upstream file
// @Description 更新网站 upstream 文件
// @Accept json
// @Param request body request.WebsiteLBUpdateFile true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/lbs/file [post]
func (b *BaseApi) UpdateLoadBalanceFile(c *gin.Context) {
var req request.WebsiteLBUpdateFile
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteService.UpdateLoadBalanceFile(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

@ -69,6 +69,7 @@ type NginxUpstream struct {
Name string `json:"name"`
Algorithm string `json:"algorithm"`
Servers []NginxUpstreamServer `json:"servers"`
Content string `json:"content"`
}
type NginxUpstreamServer struct {

@ -264,7 +264,20 @@ type WebsiteLBCreate struct {
Servers []dto.NginxUpstreamServer `json:"servers"`
}
type WebsiteLBUpdate struct {
WebsiteID uint `json:"websiteID" validate:"required"`
Name string `json:"name" validate:"required"`
Algorithm string `json:"algorithm"`
Servers []dto.NginxUpstreamServer `json:"servers"`
}
type WebsiteLBDelete struct {
WebsiteID uint `json:"websiteID" validate:"required"`
Name string `json:"name" validate:"required"`
}
type WebsiteLBUpdateFile struct {
WebsiteID uint `json:"websiteID" validate:"required"`
Name string `json:"name" validate:"required"`
Content string `json:"content" validate:"required"`
}

@ -112,6 +112,8 @@ type IWebsiteService interface {
GetLoadBalances(id uint) ([]dto.NginxUpstream, error)
CreateLoadBalance(req request.WebsiteLBCreate) error
DeleteLoadBalance(req request.WebsiteLBDelete) error
UpdateLoadBalance(req request.WebsiteLBUpdate) error
UpdateLoadBalanceFile(req request.WebsiteLBUpdateFile) error
}
func NewIWebsiteService() IWebsiteService {
@ -2934,6 +2936,11 @@ func (w WebsiteService) GetLoadBalances(id uint) ([]dto.NginxUpstream, error) {
Name: upstreamName,
}
upstreamPath := path.Join(includeDir, name)
content, err := fileOp.GetContent(upstreamPath)
if err != nil {
return nil, err
}
upstream.Content = string(content)
nginxParser, err := parser.NewParser(upstreamPath)
if err != nil {
return nil, err
@ -3019,6 +3026,9 @@ func (w WebsiteService) CreateLoadBalance(req request.WebsiteLBCreate) error {
upstream := components.Upstream{
UpstreamName: req.Name,
}
if req.Algorithm != "default" {
upstream.UpdateDirective(req.Algorithm, []string{})
}
servers := make([]*components.UpstreamServer, 0)
@ -3064,7 +3074,7 @@ func (w WebsiteService) CreateLoadBalance(req request.WebsiteLBCreate) error {
return nil
}
func (w WebsiteService) DeleteLoadBalance(req request.WebsiteLBDelete) error {
func (w WebsiteService) UpdateLoadBalance(req request.WebsiteLBUpdate) error {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
@ -3079,11 +3089,107 @@ func (w WebsiteService) DeleteLoadBalance(req request.WebsiteLBDelete) error {
if !fileOp.Stat(filePath) {
return nil
}
if err = fileOp.DeleteFile(filePath); err != nil {
parser, err := parser.NewParser(filePath)
if err != nil {
return err
}
config, err := parser.Parse()
if err != nil {
return err
}
upstreams := config.FindUpstreams()
for _, up := range upstreams {
if up.UpstreamName == req.Name {
directives := up.GetDirectives()
for _, d := range directives {
dName := d.GetName()
if _, ok := dto.LBAlgorithms[dName]; ok {
up.RemoveDirective(dName, nil)
}
}
if req.Algorithm != "default" {
up.UpdateDirective(req.Algorithm, []string{})
}
var servers []*components.UpstreamServer
for _, server := range req.Servers {
upstreamServer := &components.UpstreamServer{
Address: server.Server,
}
parameters := make(map[string]string)
if server.Weight > 0 {
parameters["weight"] = strconv.Itoa(server.Weight)
}
if server.MaxFails > 0 {
parameters["max_fails"] = strconv.Itoa(server.MaxFails)
}
if server.FailTimeout != "" {
parameters["fail_timeout"] = server.FailTimeout
}
if server.MaxConns > 0 {
parameters["max_conns"] = strconv.Itoa(server.MaxConns)
}
if server.Flag != "" {
upstreamServer.Flags = []string{server.Flag}
}
upstreamServer.Parameters = parameters
servers = append(servers, upstreamServer)
}
up.UpstreamServers = servers
}
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr(constant.ErrUpdateBuWebsite, err)
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
return err
}
return nil
}
func (w WebsiteService) DeleteLoadBalance(req request.WebsiteLBDelete) error {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "upstream")
fileOp := files.NewFileOp()
filePath := path.Join(includeDir, fmt.Sprintf("%s.conf", req.Name))
if !fileOp.Stat(filePath) {
return nil
}
if err = fileOp.DeleteFile(filePath); err != nil {
return err
}
return opNginx(nginxInstall.ContainerName, constant.NginxReload)
}
func (w WebsiteService) UpdateLoadBalanceFile(req request.WebsiteLBUpdateFile) error {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "upstream")
filePath := path.Join(includeDir, fmt.Sprintf("%s.conf", req.Name))
fileOp := files.NewFileOp()
oldContent, err := fileOp.GetContent(filePath)
if err != nil {
return err
}
if err = fileOp.WriteFile(filePath, strings.NewReader(req.Content), 0755); err != nil {
return err
}
defer func() {
if err != nil {
_ = fileOp.WriteFile(filePath, bytes.NewReader(oldContent), 0755)
}
}()
return opNginx(nginxInstall.ContainerName, constant.NginxReload)
}

@ -72,5 +72,7 @@ func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) {
websiteRouter.GET("/:id/lbs", baseApi.GetLoadBalances)
websiteRouter.POST("/lbs/create", baseApi.CreateLoadBalance)
websiteRouter.POST("/lbs/del", baseApi.DeleteLoadBalance)
websiteRouter.POST("/lbs/update", baseApi.UpdateLoadBalance)
websiteRouter.POST("/lbs/file", baseApi.UpdateLoadBalanceFile)
}
}

@ -82,10 +82,10 @@ func (us *Upstream) FindDirectives(directiveName string) []IDirective {
}
func (us *Upstream) UpdateDirective(key string, params []string) {
if key == "" || len(params) == 0 {
if key == "" {
return
}
directives := us.GetDirectives()
directives := us.Directives
index := -1
for i, dir := range directives {
if dir.GetName() == key {
@ -112,7 +112,7 @@ func (us *Upstream) UpdateDirective(key string, params []string) {
}
func (us *Upstream) RemoveDirective(key string, params []string) {
directives := us.GetDirectives()
directives := us.Directives
var newDirectives []IDirective
for _, dir := range directives {
if dir.GetName() == key {

@ -573,6 +573,14 @@ export namespace Website {
name: string;
algorithm: string;
servers: NginxUpstreamServer[];
content?: string;
websiteID?: number;
}
export interface NginxUpstreamFile {
name: string;
content: string;
websiteID: number;
}
export interface LoadBalanceReq {
@ -595,4 +603,10 @@ export namespace Website {
websiteID: number;
name: string;
}
export interface WebsiteLBUpdateFile {
websiteID: number;
name: string;
content: string;
}
}

@ -307,3 +307,11 @@ export const CreateLoadBalance = (req: Website.LoadBalanceReq) => {
export const DeleteLoadBalance = (req: Website.LoadBalanceDel) => {
return http.post(`/websites/lbs/del`, req);
};
export const UpdateLoadBalance = (req: Website.LoadBalanceReq) => {
return http.post(`/websites/lbs/update`, req);
};
export const UpdateLoadBalanceFile = (req: Website.WebsiteLBUpdateFile) => {
return http.post(`/websites/lbs/file`, req);
};

@ -0,0 +1,67 @@
<template>
<DrawerPro v-model="open" :header="$t('website.proxyFile')" :back="handleClose" :size="mobile ? 'full' : 'normal'">
<CodemirrorPro v-model="req.content" mode="nginx"></CodemirrorPro>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { computed, reactive, ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { UpdateLoadBalanceFile } from '@/api/modules/website';
import { GlobalStore } from '@/store';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import { Website } from '@/api/interface/website';
const globalStore = GlobalStore();
const mobile = computed(() => {
return globalStore.isMobile();
});
const proxyForm = ref<FormInstance>();
const open = ref(false);
const loading = ref(false);
const em = defineEmits(['close']);
const handleClose = () => {
proxyForm.value?.resetFields();
open.value = false;
em('close', false);
};
const req = reactive({
name: '',
websiteID: 0,
content: '',
});
const acceptParams = async (ups: Website.NginxUpstreamFile) => {
req.name = ups.name;
req.websiteID = ups.websiteID;
req.content = ups.content;
open.value = true;
};
const submit = async () => {
loading.value = true;
UpdateLoadBalanceFile(req)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

@ -7,7 +7,11 @@
</el-button>
</template>
<el-table-column :label="$t('commons.table.name')" prop="name"></el-table-column>
<el-table-column :label="$t('website.algorithm')" prop="algorithm"></el-table-column>
<el-table-column :label="$t('website.algorithm')" prop="algorithm">
<template #default="{ row }">
{{ getAlgorithm(row.algorithm) }}
</template>
</el-table-column>
<el-table-column :label="$t('website.server')" prop="servers" minWidth="400px">
<template #default="{ row }">
<table>
@ -45,8 +49,9 @@
/>
</ComplexTable>
</div>
<Operate ref="operateRef" @search="search()"></Operate>
<Operate ref="operateRef" @close="search()" />
<OpDialog ref="delRef" @search="search()" />
<File ref="fileRef" @search="search()" />
</template>
<script setup lang="ts">
@ -55,6 +60,8 @@ import { defineProps, onMounted, ref } from 'vue';
import Operate from './operate/index.vue';
import i18n from '@/lang';
import { Website } from '@/api/interface/website';
import { Algorithms } from '@/global/mimetype';
import File from './file/index.vue';
const props = defineProps({
id: {
@ -67,8 +74,21 @@ const data = ref([]);
const loading = ref(false);
const operateRef = ref();
const delRef = ref();
const fileRef = ref();
const buttons = [
{
label: i18n.global.t('website.proxyFile'),
click: function (row: any) {
openEditFile(row);
},
},
{
label: i18n.global.t('commons.button.edit'),
click: (row: any) => {
update(row);
},
},
{
label: i18n.global.t('commons.button.delete'),
click: (row: any) => {
@ -83,6 +103,19 @@ const search = () => {
});
};
const getAlgorithm = (key: string) => {
let label = '';
Algorithms.forEach((algorithm) => {
if (algorithm.value === key) {
label = algorithm.label;
}
});
if (label === '') {
return i18n.global.t('commons.table.default');
}
return label;
};
const deleteLb = async (row: Website.NginxUpstream) => {
delRef.value.acceptParams({
title: i18n.global.t('commons.msg.deleteTitle'),
@ -97,7 +130,26 @@ const deleteLb = async (row: Website.NginxUpstream) => {
};
const create = () => {
operateRef.value.acceptParams(props.id);
operateRef.value.acceptParams({
websiteID: props.id,
operate: 'create',
});
};
const update = (row: Website.NginxUpstream) => {
operateRef.value.acceptParams({
websiteID: props.id,
operate: 'edit',
upstream: row,
});
};
const openEditFile = (row: Website.NginxUpstream) => {
fileRef.value.acceptParams({
websiteID: props.id,
name: row.name,
content: row.content,
});
};
onMounted(() => {

@ -1,5 +1,11 @@
<template>
<DrawerPro v-model="open" :header="$t('website.addDomain')" :back="handleClose" size="large">
<DrawerPro
v-model="open"
:back="handleClose"
size="large"
:header="$t('commons.button.' + item.operate) + $t('website.loadBalance')"
:resource="item.operate == 'create' ? '' : item.name"
>
<el-form ref="lbForm" label-position="top" :model="item" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="item.name" :disabled="item.operate === 'edit'"></el-input>
@ -100,13 +106,14 @@
</template>
<script lang="ts" setup>
import { CreateLoadBalance } from '@/api/modules/website';
import { CreateLoadBalance, UpdateLoadBalance } from '@/api/modules/website';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { Algorithms, StatusStrategy } from '@/global/mimetype';
import { Website } from '@/api/interface/website';
const rules = ref<any>({
name: [Rules.linuxName],
@ -120,6 +127,12 @@ const rules = ref<any>({
maxConns: [checkNumberRange(1, 1000)],
});
interface LoadBalanceOperate {
websiteID: number;
operate: string;
upstream?: Website.NginxUpstream;
}
const lbForm = ref<FormInstance>();
const initServer = () => ({
@ -163,27 +176,51 @@ const removeServer = (index: number) => {
item.value.servers.splice(index, 1);
};
const acceptParams = async (websiteId: number) => {
item.value.websiteID = Number(websiteId);
item.value.servers = [initServer()];
const acceptParams = async (req: LoadBalanceOperate) => {
item.value.websiteID = req.websiteID;
if (req.operate == 'edit') {
item.value.operate = 'edit';
item.value.name = req.upstream?.name || '';
item.value.algorithm = req.upstream?.algorithm || 'default';
let servers = [];
req.upstream?.servers?.forEach((server) => {
const weight = server.weight == 0 ? undefined : server.weight;
const maxFails = server.maxFails == 0 ? undefined : server.maxFails;
const maxConns = server.maxConns == 0 ? undefined : server.maxConns;
servers.push({
server: server.server,
weight: weight,
maxFails: maxFails,
maxConns: maxConns,
flag: server.flag,
});
});
item.value.servers = servers;
} else {
item.value.servers = [initServer()];
}
open.value = true;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
await formEl.validate(async (valid) => {
if (!valid) {
return;
}
loading.value = true;
CreateLoadBalance(item.value)
.then(() => {
try {
if (item.value.operate === 'edit') {
await UpdateLoadBalance(item.value);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} else {
await CreateLoadBalance(item.value);
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
}
handleClose();
} finally {
loading.value = false;
}
});
};

Loading…
Cancel
Save