diff --git a/agent/app/api/v2/website.go b/agent/app/api/v2/website.go index 935676f54..df27953c9 100644 --- a/agent/app/api/v2/website.go +++ b/agent/app/api/v2/website.go @@ -855,3 +855,65 @@ func (b *BaseApi) UpdateDefaultHtml(c *gin.Context) { } helper.SuccessWithOutData(c) } + +// @Tags Website +// @Summary Get website upstreams +// @Description 获取网站 upstreams +// @Accept json +// @Param request body request.WebsiteCommonReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/lbs [get] +func (b *BaseApi) GetLoadBalances(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil) + return + } + res, err := websiteService.GetLoadBalances(id) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Create website upstream +// @Description 创建网站 upstream +// @Accept json +// @Param request body request.WebsiteLBCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/lbs/create [post] +func (b *BaseApi) CreateLoadBalance(c *gin.Context) { + var req request.WebsiteLBCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.CreateLoadBalance(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags Website +// @Summary Delete website upstream +// @Description 删除网站 upstream +// @Accept json +// @Param request body request.WebsiteLBDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/lbs/delete [post] +func (b *BaseApi) DeleteLoadBalance(c *gin.Context) { + var req request.WebsiteLBDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.DeleteLoadBalance(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/agent/app/dto/nginx.go b/agent/app/dto/nginx.go index 129f6ecfe..26627a29f 100644 --- a/agent/app/dto/nginx.go +++ b/agent/app/dto/nginx.go @@ -64,3 +64,20 @@ var StaticFileKeyMap = map[NginxKey]struct { CACHE: {}, ProxyCache: {}, } + +type NginxUpstream struct { + Name string `json:"name"` + Algorithm string `json:"algorithm"` + Servers []NginxUpstreamServer `json:"servers"` +} + +type NginxUpstreamServer struct { + Server string `json:"server"` + Weight int `json:"weight"` + FailTimeout string `json:"failTimeout"` + MaxFails int `json:"maxFails"` + MaxConns int `json:"maxConns"` + Flag string `json:"flag"` +} + +var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}} diff --git a/agent/app/dto/request/website.go b/agent/app/dto/request/website.go index 19205902d..674042d42 100644 --- a/agent/app/dto/request/website.go +++ b/agent/app/dto/request/website.go @@ -256,3 +256,15 @@ type WebsiteHtmlUpdate struct { Type string `json:"type" validate:"required"` Content string `json:"content" validate:"required"` } + +type WebsiteLBCreate 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"` +} diff --git a/agent/app/service/app_install.go b/agent/app/service/app_install.go index e7311dd3f..70ea0f32a 100644 --- a/agent/app/service/app_install.go +++ b/agent/app/service/app_install.go @@ -726,6 +726,22 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) { } } appParam.Values = form.Values + } else if form.Type == "apps" { + if m, ok := form.Child.(map[string]interface{}); ok { + result := make(map[string]string) + for key, value := range m { + if strVal, ok := value.(string); ok { + result[key] = strVal + } + } + if envKey, ok := result["envKey"]; ok { + serviceName := envs[envKey] + if serviceName != nil { + appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithServiceName(serviceName.(string))) + appParam.ShowValue = appInstall.Name + } + } + } } params = append(params, appParam) } else { diff --git a/agent/app/service/website.go b/agent/app/service/website.go index 61dbd3bcb..29f2f1eaa 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -108,6 +108,10 @@ type IWebsiteService interface { UpdateDefaultHtml(req request.WebsiteHtmlUpdate) error GetDefaultHtml(resourceType string) (*response.WebsiteHtmlRes, error) + + GetLoadBalances(id uint) ([]dto.NginxUpstream, error) + CreateLoadBalance(req request.WebsiteLBCreate) error + DeleteLoadBalance(req request.WebsiteLBDelete) error } func NewIWebsiteService() IWebsiteService { @@ -2898,6 +2902,188 @@ func (w WebsiteService) UpdateDefaultHtml(req request.WebsiteHtmlUpdate) error { return fileOp.SaveFile(resourcePath, req.Content, 0644) } -func (w WebsiteService) GetUpStreams() ([]dto.NginxUpstream, error) { - return nil, nil +func (w WebsiteService) GetLoadBalances(id uint) ([]dto.NginxUpstream, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return nil, err + } + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "upstream") + fileOp := files.NewFileOp() + if !fileOp.Stat(includeDir) { + return nil, nil + } + entries, err := os.ReadDir(includeDir) + if err != nil { + return nil, err + } + var res []dto.NginxUpstream + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".conf") { + continue + } + upstreamName := strings.TrimSuffix(name, ".conf") + upstream := dto.NginxUpstream{ + Name: upstreamName, + } + upstreamPath := path.Join(includeDir, name) + nginxParser, err := parser.NewParser(upstreamPath) + if err != nil { + return nil, err + } + config, err := nginxParser.Parse() + if err != nil { + return nil, err + } + upstreams := config.FindUpstreams() + for _, up := range upstreams { + if up.UpstreamName == upstreamName { + directives := up.GetDirectives() + for _, d := range directives { + dName := d.GetName() + if _, ok := dto.LBAlgorithms[dName]; ok { + upstream.Algorithm = dName + } + } + var servers []dto.NginxUpstreamServer + for _, ups := range up.UpstreamServers { + server := dto.NginxUpstreamServer{ + Server: ups.Address, + } + parameters := ups.Parameters + if weight, ok := parameters["weight"]; ok { + num, err := strconv.Atoi(weight) + if err == nil { + server.Weight = num + } + } + if maxFails, ok := parameters["max_fails"]; ok { + num, err := strconv.Atoi(maxFails) + if err == nil { + server.MaxFails = num + } + } + if failTimeout, ok := parameters["fail_timeout"]; ok { + server.FailTimeout = failTimeout + } + if maxConns, ok := parameters["max_conns"]; ok { + num, err := strconv.Atoi(maxConns) + if err == nil { + server.MaxConns = num + } + } + for _, flag := range ups.Flags { + server.Flag = flag + } + servers = append(servers, server) + } + upstream.Servers = servers + } + } + res = append(res, upstream) + } + return res, nil +} + +func (w WebsiteService) CreateLoadBalance(req request.WebsiteLBCreate) 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() + if !fileOp.Stat(includeDir) { + _ = fileOp.CreateDir(includeDir, 0644) + } + filePath := path.Join(includeDir, fmt.Sprintf("%s.conf", req.Name)) + if fileOp.Stat(filePath) { + return buserr.New(constant.ErrNameIsExist) + } + config, err := parser.NewStringParser(string(nginx_conf.Upstream)).Parse() + if err != nil { + return err + } + config.Block = &components.Block{} + config.FilePath = filePath + upstream := components.Upstream{ + UpstreamName: req.Name, + } + + servers := make([]*components.UpstreamServer, 0) + + 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) + } + upstream.UpstreamServers = servers + config.Block.Directives = append(config.Block.Directives, &upstream) + + defer func() { + if err != nil { + _ = fileOp.DeleteFile(filePath) + } + }() + + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return buserr.WithErr(constant.ErrUpdateBuWebsite, err) + } + nginxInclude := fmt.Sprintf("/www/sites/%s/upstream/*.conf", website.Alias) + if err = updateNginxConfig("", []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); 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 + } + if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil { + return err + } + return nil } diff --git a/agent/cmd/server/nginx_conf/nginx_conf.go b/agent/cmd/server/nginx_conf/nginx_conf.go index abee3b724..ab265cbe4 100644 --- a/agent/cmd/server/nginx_conf/nginx_conf.go +++ b/agent/cmd/server/nginx_conf/nginx_conf.go @@ -40,3 +40,6 @@ var StopHTML []byte //go:embed path_auth.conf var PathAuth []byte + +//go:embed upstream.conf +var Upstream []byte diff --git a/agent/cmd/server/nginx_conf/upstream.conf b/agent/cmd/server/nginx_conf/upstream.conf new file mode 100644 index 000000000..4d66cafcd --- /dev/null +++ b/agent/cmd/server/nginx_conf/upstream.conf @@ -0,0 +1,3 @@ +upstream backend { + server backend1.example.com; +} \ No newline at end of file diff --git a/agent/router/ro_website.go b/agent/router/ro_website.go index f516b09d7..f5df8a7f9 100644 --- a/agent/router/ro_website.go +++ b/agent/router/ro_website.go @@ -68,5 +68,9 @@ func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) { websiteRouter.GET("/default/html/:type", baseApi.GetDefaultHtml) websiteRouter.POST("/default/html/update", baseApi.UpdateDefaultHtml) + + websiteRouter.GET("/:id/lbs", baseApi.GetLoadBalances) + websiteRouter.POST("/lbs/create", baseApi.CreateLoadBalance) + websiteRouter.POST("/lbs/del", baseApi.DeleteLoadBalance) } } diff --git a/agent/utils/nginx/components/config.go b/agent/utils/nginx/components/config.go index 7f9b5bce9..48bd1372a 100644 --- a/agent/utils/nginx/components/config.go +++ b/agent/utils/nginx/components/config.go @@ -24,6 +24,15 @@ func (c *Config) FindHttp() *Http { return http } +func (c *Config) FindUpstreams() []*Upstream { + var upstreams []*Upstream + directives := c.Block.FindDirectives("upstream") + for _, directive := range directives { + upstreams = append(upstreams, directive.(*Upstream)) + } + return upstreams +} + var repeatKeys = map[string]struct { }{ "limit_conn": {}, diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index 12b59def2..1cf34dd2f 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -568,4 +568,31 @@ export namespace Website { type: string; content: string; } + + export interface NginxUpstream { + name: string; + algorithm: string; + servers: NginxUpstreamServer[]; + } + + export interface LoadBalanceReq { + websiteID: number; + name: string; + algorithm: string; + servers: NginxUpstreamServer[]; + } + + interface NginxUpstreamServer { + server: string; + weight: number; + failTimeout: string; + maxFails: number; + maxConns: number; + flag: string; + } + + export interface LoadBalanceDel { + websiteID: number; + name: string; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index eff283569..1b35b9449 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -295,3 +295,15 @@ export const DownloadCAFile = (params: Website.SSLDownload) => { timeout: TimeoutEnum.T_40S, }); }; + +export const GetLoadBalances = (id: number) => { + return http.get(`/websites/${id}/lbs`); +}; + +export const CreateLoadBalance = (req: Website.LoadBalanceReq) => { + return http.post(`/websites/lbs/create`, req); +}; + +export const DeleteLoadBalance = (req: Website.LoadBalanceDel) => { + return http.post(`/websites/lbs/del`, req); +}; diff --git a/frontend/src/global/mimetype.ts b/frontend/src/global/mimetype.ts index 11a9615f1..ef1bae443 100644 --- a/frontend/src/global/mimetype.ts +++ b/frontend/src/global/mimetype.ts @@ -265,3 +265,37 @@ export const Actions = [ value: 'five_seconds', }, ]; + +export const Algorithms = [ + { + label: i18n.global.t('commons.table.default'), + value: 'default', + placeHolder: i18n.global.t('website.defaultHelper'), + }, + { + label: i18n.global.t('website.ipHash'), + value: 'ip_hash', + placeHolder: i18n.global.t('website.ipHashHelper'), + }, + { + label: i18n.global.t('website.leastConn'), + value: 'least_conn', + placeHolder: i18n.global.t('website.leastConnHelper'), + }, + { + label: i18n.global.t('website.leastTime'), + value: 'least_time', + placeHolder: i18n.global.t('website.leastTimeHelper'), + }, +]; + +export const StatusStrategy = [ + { + label: i18n.global.t('website.strategyDown'), + value: 'down', + }, + { + label: i18n.global.t('website.strategyBackup'), + value: 'backup', + }, +]; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 07aefb7e7..8503211db 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2139,6 +2139,24 @@ const message = { subsiteHelper: 'A subsite can select an existing PHP or static website directory as the main directory.', parentWbeiste: 'Parent Website', deleteSubsite: 'To delete the current website, you must first delete the subsite(s) {0}', + loadBalance: 'Load Balancing', + server: 'Server', + algorithm: 'Algorithm', + ipHash: 'IP Hash', + ipHashHelper: + 'Distributes requests to a specific server based on the client IP address, ensuring that a particular client is always routed to the same server.', + leastConn: 'Least Connections', + leastConnHelper: 'Sends requests to the server with the fewest active connections.', + leastTime: 'Least Time', + leastTimeHelper: 'Sends requests to the server with the shortest active connection time.', + defaultHelper: + 'Default method, requests are evenly distributed to each server. If servers have weights configured, requests are distributed based on the specified weights, with higher-weighted servers receiving more requests.', + weight: 'Weight', + maxFails: 'Max Fails', + maxConns: 'Max Connections', + strategy: 'Strategy', + strategyDown: 'Down', + strategyBackup: 'Backup', }, php: { short_open_tag: 'Short tag support', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index df5ab6850..df0973c31 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1988,6 +1988,23 @@ const message = { subsiteHelper: '子網站可以選擇已存在的 PHP 和靜態網站的目錄作為主目錄。', parentWbeiste: '父級網站', deleteSubsite: '刪除當前網站需要先刪除子網站 {0}', + loadBalance: '負載均衡', + server: '節點', + algorithm: '演算法', + ipHash: 'IP 雜湊', + ipHashHelper: '基於客戶端 IP 位址將請求分配到特定伺服器,可以確保特定客戶端總是被路由到同一伺服器。', + leastConn: '最少連接', + leastConnHelper: '將請求發送到當前活動連接數最少的伺服器。', + leastTime: '最少時間', + leastTimeHelper: '將請求發送到當前活動連接時間最短的伺服器。', + defaultHelper: + '預設方法,請求被均勻分配到每個伺服器。如果伺服器有權重配置,則根據指定的權重分配請求,權重越高的伺服器接收更多請求。', + weight: '權重', + maxFails: '最大失敗次數', + maxConns: '最大連接數', + strategy: '策略', + strategyDown: '停用', + strategyBackup: '備用', }, php: { short_open_tag: '短標簽支持', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 5d037213b..9bc810d61 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1990,6 +1990,23 @@ const message = { subsiteHelper: '子网站可以选择已存在的 PHP 和静态网站的目录作为主目录', parentWbeiste: '父级网站', deleteSubsite: '删除当前网站需要先删除子网站 {0}', + loadBalance: '负载均衡', + server: '节点', + algorithm: '算法', + ipHash: 'IP 哈希', + ipHashHelper: '基于客户端 IP 地址将请求分配到特定服务器,可以确保特定客户端总是被路由到同一服务器', + leastConn: '最小连接', + leastConnHelper: '将请求发送到当前活动连接数最少的服务器', + leastTime: '最小时间', + leastTimeHelper: '将请求发送到当前活动连接时间最短的服务器', + defaultHelper: + '默认方法,请求被均匀分配到每个服务器,如果服务器有权重配置,则根据指定的权重分配请求,权重越高的服务器接收更多请求', + weight: '权重', + maxFails: '最大失败次数', + maxConns: '最大连接数', + strategy: '策略', + strategyDown: '停用', + strategyBackup: '备用', }, php: { short_open_tag: '短标签支持', diff --git a/frontend/src/views/website/website/config/basic/auth-basic/index.vue b/frontend/src/views/website/website/config/basic/auth-basic/index.vue index fe43446d3..7c48d6d37 100644 --- a/frontend/src/views/website/website/config/basic/auth-basic/index.vue +++ b/frontend/src/views/website/website/config/basic/auth-basic/index.vue @@ -203,7 +203,6 @@ const searchPath = async () => { const searchAll = () => { search(); searchPath(); - console.log(11111); }; onMounted(() => { diff --git a/frontend/src/views/website/website/config/basic/domain/index.vue b/frontend/src/views/website/website/config/basic/domain/index.vue index b61840a8a..eb1fdcd9d 100644 --- a/frontend/src/views/website/website/config/basic/domain/index.vue +++ b/frontend/src/views/website/website/config/basic/domain/index.vue @@ -99,7 +99,7 @@ const deleteDomain = async (row: Website.Domain) => { names: [row.domain], msg: i18n.global.t('commons.msg.operatorHelper', [ i18n.global.t('website.domain'), - i18n.global.t('commons.msg.delete'), + i18n.global.t('commons.button.delete'), ]), api: DeleteDomain, params: { id: row.id }, diff --git a/frontend/src/views/website/website/config/basic/index.vue b/frontend/src/views/website/website/config/basic/index.vue index 3d5fb99ec..aaffeb36b 100644 --- a/frontend/src/views/website/website/config/basic/index.vue +++ b/frontend/src/views/website/website/config/basic/index.vue @@ -15,23 +15,26 @@ + + + - + - + - + - + - + - + @@ -50,6 +53,7 @@ import Proxy from './proxy/index.vue'; import AuthBasic from './auth-basic/index.vue'; import AntiLeech from './anti-Leech/index.vue'; import Redirect from './redirect/index.vue'; +import LoadBalance from './load-balance/index.vue'; const props = defineProps({ id: { diff --git a/frontend/src/views/website/website/config/basic/load-balance/index.vue b/frontend/src/views/website/website/config/basic/load-balance/index.vue new file mode 100644 index 000000000..f211b5806 --- /dev/null +++ b/frontend/src/views/website/website/config/basic/load-balance/index.vue @@ -0,0 +1,106 @@ + + + diff --git a/frontend/src/views/website/website/config/basic/load-balance/operate/index.vue b/frontend/src/views/website/website/config/basic/load-balance/operate/index.vue new file mode 100644 index 000000000..38644ea9f --- /dev/null +++ b/frontend/src/views/website/website/config/basic/load-balance/operate/index.vue @@ -0,0 +1,193 @@ + + + diff --git a/frontend/src/views/website/website/config/basic/rewrite/index.vue b/frontend/src/views/website/website/config/basic/rewrite/index.vue index ce2977867..32310c49c 100644 --- a/frontend/src/views/website/website/config/basic/rewrite/index.vue +++ b/frontend/src/views/website/website/config/basic/rewrite/index.vue @@ -12,7 +12,7 @@ {{ $t('website.rewriteHelper2') }} - +
diff --git a/frontend/src/views/website/website/domain-create/index.vue b/frontend/src/views/website/website/domain-create/index.vue index 800cfdaf2..bb8f3515e 100644 --- a/frontend/src/views/website/website/domain-create/index.vue +++ b/frontend/src/views/website/website/domain-create/index.vue @@ -50,7 +50,7 @@ - +