diff --git a/biz/master/proxy/update_proxy_config.go b/biz/master/proxy/update_proxy_config.go index 3c13e20..eb8b34b 100644 --- a/biz/master/proxy/update_proxy_config.go +++ b/biz/master/proxy/update_proxy_config.go @@ -5,6 +5,7 @@ import ( "github.com/VaalaCat/frp-panel/biz/master/client" "github.com/VaalaCat/frp-panel/common" + "github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/models" "github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/services/app" @@ -12,6 +13,7 @@ import ( "github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils/logger" v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" "github.com/samber/lo" ) @@ -71,7 +73,13 @@ func UpdateProxyConfig(c *app.Context, req *pb.UpdateProxyConfigRequest) (*pb.Up return nil, fmt.Errorf("invalid config") } - if err := proxyCfg.FillTypedProxyConfig(typedProxyCfgs[0]); err != nil { + typedProxyCfg := typedProxyCfgs[0] + + if typedProxyCfg.GetBaseConfig().Type == string(v1.ProxyTypeHTTP) { + typedProxyCfg = UpdateWorkerLoadBalancerGroup(typedProxyCfg) + } + + if err := proxyCfg.FillTypedProxyConfig(typedProxyCfg); err != nil { logger.Logger(c).WithError(err).Errorf("cannot fill typed proxy config") return nil, err } @@ -96,9 +104,9 @@ func UpdateProxyConfig(c *app.Context, req *pb.UpdateProxyConfigRequest) (*pb.Up return nil, err } else { oldCfg.Proxies = lo.Filter(oldCfg.Proxies, func(proxy v1.TypedProxyConfig, _ int) bool { - return proxy.GetBaseConfig().Name != typedProxyCfgs[0].GetBaseConfig().Name + return proxy.GetBaseConfig().Name != typedProxyCfg.GetBaseConfig().Name }) - oldCfg.Proxies = append(oldCfg.Proxies, typedProxyCfgs...) + oldCfg.Proxies = append(oldCfg.Proxies, typedProxyCfg) if err := clientEntity.SetConfigContent(*oldCfg); err != nil { logger.Logger(c).WithError(err).Errorf("cannot set client config, id: [%s]", clientID) @@ -128,3 +136,28 @@ func UpdateProxyConfig(c *app.Context, req *pb.UpdateProxyConfigRequest) (*pb.Up Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"}, }, nil } + +func UpdateWorkerLoadBalancerGroup(typedProxyCfg v1.TypedProxyConfig) v1.TypedProxyConfig { + annotations := typedProxyCfg.GetBaseConfig().Annotations + workerId := "" + if len(annotations) > 0 { + if annotations[defs.FrpProxyAnnotationsKey_Ingress] != "" && len(annotations[defs.FrpProxyAnnotationsKey_WorkerId]) > 0 { + workerId = annotations[defs.FrpProxyAnnotationsKey_WorkerId] + } + } + httpProxyCfg := &v1.HTTPProxyConfig{} + msg := &msg.NewProxy{} + typedProxyCfg.ProxyConfigurer.MarshalToMsg(msg) + httpProxyCfg.UnmarshalFromMsg(msg) + + if len(workerId) > 0 { + httpProxyCfg.LoadBalancer = v1.LoadBalancerConfig{ + Group: models.HttpIngressLBGroup(workerId, httpProxyCfg), + GroupKey: workerId, + } + } + + typedProxyCfg.ProxyConfigurer = httpProxyCfg + + return typedProxyCfg +} diff --git a/biz/master/worker/create_worker_ingress.go b/biz/master/worker/create_worker_ingress.go index 3a67826..411c015 100644 --- a/biz/master/worker/create_worker_ingress.go +++ b/biz/master/worker/create_worker_ingress.go @@ -74,6 +74,11 @@ func CreateWorkerIngress(ctx *app.Context, req *pb.CreateWorkerIngressRequest) ( }, } + httpProxyCfg.LoadBalancer = v1.LoadBalancerConfig{ + Group: models.HttpIngressLBGroup(workerId, &httpProxyCfg), + GroupKey: workerId, + } + if err := proxy.CreateProxyConfigWithTypedConfig(ctx, proxy.CreateProxyConfigWithTypedConfigParam{ ClientID: clientId, ServerID: serverId, diff --git a/defs/const.go b/defs/const.go index aa63b2c..3d9ed62 100644 --- a/defs/const.go +++ b/defs/const.go @@ -171,6 +171,7 @@ const ( ) const ( - FrpProxyAnnotationsKey_Ingress = "ingress" - FrpProxyAnnotationsKey_WorkerId = "worker_id" + FrpProxyAnnotationsKey_Ingress = "ingress" + FrpProxyAnnotationsKey_WorkerId = "worker_id" + FrpProxyAnnotationsKey_LoadBalancerGroup = "load_balancer_group" ) diff --git a/models/worker.go b/models/worker.go index 39a910a..7a2eb69 100644 --- a/models/worker.go +++ b/models/worker.go @@ -1,9 +1,12 @@ package models import ( + "fmt" "time" "github.com/VaalaCat/frp-panel/pb" + "github.com/VaalaCat/frp-panel/utils" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/samber/lo" "gorm.io/gorm" ) @@ -84,3 +87,7 @@ func (w *Worker) ToPB() *pb.Worker { ret := w.WorkerEntity.ToPB() return ret } + +func HttpIngressLBGroup(workerId string, cfg *v1.HTTPProxyConfig) string { + return fmt.Sprintf("lb-group-%s-%s", workerId, utils.MD5(fmt.Sprint(cfg.DomainConfig.CustomDomains, cfg.SubDomain))) +} diff --git a/www/components/worker/worker_status.tsx b/www/components/worker/worker_status.tsx index 14d1bb8..195e20e 100644 --- a/www/components/worker/worker_status.tsx +++ b/www/components/worker/worker_status.tsx @@ -37,134 +37,137 @@ export function WorkerStatus({ workerId, clients = [], compact = false }: Worker enabled: !!workerId, }) - // 统计部署状态 + // 状态统计 const clientStatuses = statusResp?.workerStatus || {} - const deployedClients = clients || [] + const deployedClients = clients const totalClients = deployedClients.length + const runningClients = Object.values(clientStatuses).filter((s) => s === 'running').length + const Clients = Object.values(clientStatuses).filter((s) => s === '').length + const stoppedClients = Object.values(clientStatuses).filter((s) => s === 'stopped').length - const runningClients = Object.entries(clientStatuses).filter(([_, status]) => status === 'running').length - const errorClients = Object.entries(clientStatuses).filter(([_, status]) => status === 'error').length - const stoppedClients = Object.entries(clientStatuses).filter(([_, status]) => status === 'stopped').length - - // 统计入口状态 const ingresses = ingressResp?.proxyConfigs || [] const totalIngresses = ingresses.length - // 查询所有入口的状态 + // 针对每个 ingress 再次拉取状态 const ingressStatuses = useQuery({ - queryKey: ['getIngressStatuses', workerId, ingresses.map((i: ProxyConfig) => i.id).join(','), refetchTrigger], + queryKey: ['getIngressStatuses', workerId, ingresses.map((i) => i.id).join(','), refetchTrigger], queryFn: async () => { const statuses: Record = {} - await Promise.all( - ingresses.map(async (ingress: ProxyConfig) => { + ingresses.map(async (i) => { try { - const proxyStatus = await getProxyConfig({ - clientId: ingress.clientId, - serverId: ingress.serverId, - name: ingress.name, + const ps = await getProxyConfig({ + clientId: i.clientId, + serverId: i.serverId, + name: i.name, }) - statuses[ingress.id || ''] = proxyStatus?.workingStatus?.status || 'unknown' - } catch (e) { - statuses[ingress.id || ''] = 'error' + statuses[i.id || ''] = ps?.workingStatus?.status || 'unknown' + } catch { + statuses[i.id || ''] = '' } }), ) - return statuses }, enabled: ingresses.length > 0, refetchInterval: 10000, }) - const runningIngresses = Object.values(ingressStatuses.data || {}).filter((status) => status === 'running').length - const errorIngresses = Object.values(ingressStatuses.data || {}).filter((status) => - ['error', 'start error', 'check failed'].includes(status), + const runningIngresses = Object.values(ingressStatuses.data || {}).filter((s) => s === 'running').length + const Ingresses = Object.values(ingressStatuses.data || {}).filter((s) => + ['', 'start', 'check failed'].includes(s), ).length - // 计算总体状态 + // Overall 状态 const getOverallStatus = () => { if (totalClients === 0 && totalIngresses === 0) { return { variant: 'outline' as const, text: t('worker.status.no_resources'), color: 'bg-gray-100 text-gray-700' } } - - // 资源完全不可用 if ((totalClients > 0 && runningClients === 0) || (totalIngresses > 0 && runningIngresses === 0)) { return { variant: 'destructive' as const, text: t('worker.status.unusable'), color: 'bg-red-500 text-white' } } - - // 资源不健康但部分可用 - if (errorClients > 0 || errorIngresses > 0) { + if (Clients > 0 || Ingresses > 0) { return { variant: 'warning' as const, text: t('worker.status.unhealthy'), color: 'bg-amber-500 text-white' } } - - // 所有资源健康 if (runningClients === totalClients && runningIngresses === totalIngresses) { return { variant: 'default' as const, text: t('worker.status.healthy'), color: 'bg-green-500 text-white' } } - - // 部分资源降级但无错误 return { variant: 'secondary' as const, text: t('worker.status.degraded'), color: 'bg-orange-500 text-white' } } + const { text: overallText, color: overallColor } = getOverallStatus() - const { variant, text, color } = getOverallStatus() - - // 生成客户端资源状态指示器 + // per-client indicators const renderClientIndicators = () => { if (totalClients === 0) return null - + const showList = deployedClients.slice(0, 3) return ( -
-
-
- {Array.from({ length: Math.min(totalClients, 3) }).map((_, i) => ( -
- ))} -
- - - - {totalClients > 3 && +{totalClients - 3}} +
+
+ {showList.map((client) => { + const status = clientStatuses[client.id || ''] || 'unknown' + const bg = status === 'running' ? 'bg-green-500' : status === '' ? 'bg-red-500' : 'bg-gray-300' + return ( + + +
+ + +

{client.id}

+
+ {t('worker.status.clients')}: {status} +
+
+ + ) + })}
+ + + + {totalClients > 3 && +{totalClients - 3}}
) } - // 生成入口资源状态指示器 + // per-ingress indicators const renderIngressIndicators = () => { if (totalIngresses === 0) return null - + const showList = ingresses.slice(0, 3) return ( -
-
-
- {Array.from({ length: Math.min(totalIngresses, 3) }).map((_, i) => ( -
- ))} -
- - - - {totalIngresses > 3 && +{totalIngresses - 3}} +
+
+ {showList.map((ing) => { + const status = ingressStatuses.data?.[ing.id || ''] || 'unknown' + const bg = + status === 'running' + ? 'bg-green-500' + : ['', 'start', 'check failed'].includes(status) + ? 'bg-red-500' + : 'bg-gray-300' + return ( + + +
+ + +

{ing.name}

+
+ {t('worker.status.ingresses')}: {status} +
+
+ + ) + })}
+ + + + {totalIngresses > 3 && +{totalIngresses - 3}}
) } + // Compact 模式仍旧整体 hover if (compact) { return ( @@ -172,13 +175,13 @@ export function WorkerStatus({ workerId, clients = [], compact = false }: Worker
-

{text}

-
-
- {t('worker.status.clients')}: {runningClients}/{totalClients} {t('worker.status.running')} +

{overallText}

+
+
+ {t('worker.status.clients')}: {runningClients}/{totalClients}
-
- {t('worker.status.ingresses')}: {runningIngresses}/{totalIngresses} {t('worker.status.running')} +
+ {t('worker.status.ingresses')}: {runningIngresses}/{totalIngresses}
@@ -202,32 +205,51 @@ export function WorkerStatus({ workerId, clients = [], compact = false }: Worker ) } + // 默认模式:拆分整体与细节 hover return ( - - -
-
- {renderIngressIndicators()} - {renderClientIndicators()} -
- {text} -
-
- -
-

{text}

-
-
- {t('worker.status.clients')}: {runningClients}/{totalClients} {t('worker.status.running')} -
-
- {t('worker.status.ingresses')}: {runningIngresses}/{totalIngresses} {t('worker.status.running')} +
+ {renderIngressIndicators()} + {renderClientIndicators()} + + {/* 只在 Badge 上展示总体状态 */} + + + {overallText} + + +
+

{overallText}

+
+
+ + + {t('worker.status.running')}: {runningClients}/{totalClients} + +
+
+ + + {t('worker.status')}: {Clients}/{totalClients} + +
+
+ + + {t('worker.status.running')}: {runningIngresses}/{totalIngresses} + +
+
+ + + {t('worker.status')}: {Ingresses}/{totalIngresses} + +
-
- - + + +
) }