feat: support cloudflare workerd

This commit is contained in:
VaalaCat
2025-05-06 02:08:59 +00:00
parent 6949e1305a
commit 90f8884d1b
110 changed files with 8725 additions and 284 deletions

View File

@@ -0,0 +1,48 @@
name: Docker image with workerd
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: npm setup
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.24.x"
- name: npm install and build
run: |
cd www
npm install && npm install -g pnpm
- name: Install dependencies
run: |
go mod tidy
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- name: Install Protoc
uses: arduino/setup-protoc@v3
- name: Compile server
run: bash ./build.sh --current
- name: Setup ko
uses: ko-build/setup-ko@v0.9
env:
KO_DOCKER_REPO: docker.io/vaalacat/frp-panel
- name: Build image with ko
env:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
mv .ko.workerd.yaml .ko.yaml
echo "${password}" | ko login docker.io --username ${username} --password-stdin
ko build ./cmd/frpp --sbom=none --bare -t latest-workerd

16
.ko.workerd.yaml Normal file
View File

@@ -0,0 +1,16 @@
defaultBaseImage: jacoblincool/workerd
builds:
- id: frpp
dir: .
main: ./cmd/frpp
ldflags:
- -s -w
- -X github.com/VaalaCat/frp-panel/conf.buildDate={{.Date}}
- -X github.com/VaalaCat/frp-panel/conf.gitCommit={{.Git.FullCommit}}
- -X github.com/VaalaCat/frp-panel/conf.gitVersion={{.Git.Tag}}
- -X github.com/VaalaCat/frp-panel/conf.gitBranch={{.Git.Branch}}
defaultPlatforms:
- linux/arm64
- linux/amd64

View File

@@ -0,0 +1,31 @@
package client
import (
"fmt"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/workerd"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func CreateWorker(ctx *app.Context, req *pb.CreateWorkerRequest) (*pb.CreateWorkerResponse, error) {
if !ctx.GetApp().GetConfig().Client.Features.EnableFunctions {
logger.Logger(ctx).Errorf("function features are not enabled")
return nil, fmt.Errorf("function features are not enabled")
}
mgr := ctx.GetApp().GetWorkersManager()
ctrl := workerd.NewWorkerdController(req.GetWorker(), ctx.GetApp().GetConfig().Client.Worker.WorkerdWorkDir)
if err := mgr.RunWorker(ctx, req.GetWorker().GetWorkerId(), ctrl); err != nil {
return nil, err
}
logger.Logger(ctx).Infof("create worker success, id: [%s], running at: [%s]", req.GetWorker().GetWorkerId(), req.GetWorker().GetSocket().GetAddress())
return &pb.CreateWorkerResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
}

View File

@@ -0,0 +1,33 @@
package client
import (
"fmt"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func GetWorkerStatus(ctx *app.Context, req *pb.GetWorkerStatusRequest) (*pb.GetWorkerStatusResponse, error) {
if !ctx.GetApp().GetConfig().Client.Features.EnableFunctions {
logger.Logger(ctx).Errorf("function features are not enabled")
return nil, fmt.Errorf("function features are not enabled")
}
clientId := ctx.GetApp().GetConfig().Client.ID
workersMgr := ctx.GetApp().GetWorkersManager()
status, err := workersMgr.GetWorkerStatus(ctx, req.GetWorkerId())
if err != nil {
logger.Logger(ctx).Errorf("failed to get worker status: %v", err)
return nil, fmt.Errorf("failed to get worker status: %v", err)
}
logger.Logger(ctx).Infof("get worker status for worker [%s], status: [%s]", req.GetWorkerId(), status)
return &pb.GetWorkerStatusResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
WorkerStatus: map[string]string{
clientId: string(status),
},
}, nil
}

View File

@@ -0,0 +1,40 @@
package client
import (
"fmt"
"os"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func InstallWorkerd(ctx *app.Context, req *pb.InstallWorkerdRequest) (*pb.InstallWorkerdResponse, error) {
if !ctx.GetApp().GetConfig().Client.Features.EnableFunctions {
logger.Logger(ctx).Errorf("function features are not enabled")
return nil, fmt.Errorf("function features are not enabled")
}
workersMgr := ctx.GetApp().GetWorkersManager()
cwd, err := os.Getwd()
if err != nil {
logger.Logger(ctx).Errorf("failed to get current working directory: %v, will install workerd in /usr/local/bin", err)
}
binPath, err := workersMgr.InstallWorkerd(ctx, req.GetDownloadUrl(), cwd)
if err != nil {
logger.Logger(ctx).Errorf("failed to install workerd: %v", err)
return nil, fmt.Errorf("failed to install workerd: %v", err)
}
execMgr := ctx.GetApp().GetWorkerExecManager()
execMgr.UpdateBinaryPath(binPath)
return &pb.InstallWorkerdResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "ok",
},
}, nil
}

View File

@@ -0,0 +1,31 @@
package client
import (
"fmt"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func RemoveWorker(ctx *app.Context, req *pb.RemoveWorkerRequest) (*pb.RemoveWorkerResponse, error) {
if !ctx.GetApp().GetConfig().Client.Features.EnableFunctions {
logger.Logger(ctx).Errorf("function features are not enabled")
return nil, fmt.Errorf("function features are not enabled")
}
mgr := ctx.GetApp().GetWorkersManager()
workerId := req.GetWorkerId()
logger.Logger(ctx).Infof("start remove worker, id: [%s]", workerId)
if err := mgr.StopWorker(ctx, workerId); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot remove worker, id: [%s]", workerId)
return nil, err
}
logger.Logger(ctx).Infof("remove worker success, id: [%s]", workerId)
return &pb.RemoveWorkerResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
}

View File

@@ -19,7 +19,7 @@ func HandleServerMessage(appInstance app.Application, req *pb.ServerMessage) *pb
} }
}() }()
c := context.Background() c := context.Background()
logger.Logger(c).Infof("client get a server message, origin is: [%+v]", req) logger.Logger(c).Infof("client get a server message, clientId: [%s], event: [%s], sessionId: [%s]", req.GetClientId(), req.GetEvent().String(), req.GetSessionId())
switch req.Event { switch req.Event {
case pb.Event_EVENT_UPDATE_FRPC: case pb.Event_EVENT_UPDATE_FRPC:
return app.WrapperServerMsg(appInstance, req, UpdateFrpcHander) return app.WrapperServerMsg(appInstance, req, UpdateFrpcHander)
@@ -37,6 +37,14 @@ func HandleServerMessage(appInstance app.Application, req *pb.ServerMessage) *pb
return app.WrapperServerMsg(appInstance, req, StartPTYConnect) return app.WrapperServerMsg(appInstance, req, StartPTYConnect)
case pb.Event_EVENT_GET_PROXY_INFO: case pb.Event_EVENT_GET_PROXY_INFO:
return app.WrapperServerMsg(appInstance, req, GetProxyConfig) return app.WrapperServerMsg(appInstance, req, GetProxyConfig)
case pb.Event_EVENT_CREATE_WORKER:
return app.WrapperServerMsg(appInstance, req, CreateWorker)
case pb.Event_EVENT_REMOVE_WORKER:
return app.WrapperServerMsg(appInstance, req, RemoveWorker)
case pb.Event_EVENT_GET_WORKER_STATUS:
return app.WrapperServerMsg(appInstance, req, GetWorkerStatus)
case pb.Event_EVENT_INSTALL_WORKERD:
return app.WrapperServerMsg(appInstance, req, InstallWorkerd)
case pb.Event_EVENT_PING: case pb.Event_EVENT_PING:
rawData, _ := proto.Marshal(conf.GetVersion().ToProto()) rawData, _ := proto.Marshal(conf.GetVersion().ToProto())
return &pb.ClientMessage{ return &pb.ClientMessage{

View File

@@ -0,0 +1,48 @@
package client
import (
"context"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/workerd"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func PullWorkers(appInstance app.Application, clientID, clientSecret string) error {
ctx := app.NewContext(context.Background(), appInstance)
if !ctx.GetApp().GetConfig().Client.Features.EnableFunctions {
logger.Logger(ctx).Infof("function features are not enabled")
return nil
}
logger.Logger(ctx).Infof("start to pull workers belong to client, clientID: [%s]", clientID)
cli := ctx.GetApp().GetMasterCli()
resp, err := cli.Call().ListClientWorkers(ctx, &pb.ListClientWorkersRequest{
Base: &pb.ClientBase{
ClientId: clientID,
ClientSecret: clientSecret,
},
})
if err != nil {
logger.Logger(ctx).WithError(err).Error("cannot list client workers")
return err
}
if len(resp.GetWorkers()) == 0 {
logger.Logger(ctx).Infof("client [%s] has no workers", clientID)
return nil
}
ctrl := ctx.GetApp().GetWorkersManager()
for _, worker := range resp.GetWorkers() {
ctrl.RunWorker(ctx, worker.GetWorkerId(), workerd.NewWorkerdController(worker, ctx.GetApp().GetConfig().Client.Worker.WorkerdWorkDir))
}
logger.Logger(ctx).Infof("pull workers belong to client success, clientID: [%s], will run [%d] workers", clientID, len(resp.GetWorkers()))
return nil
}

View File

@@ -29,11 +29,13 @@ func StartFRPCHandler(ctx *app.Context, req *pb.StartFRPCRequest) (*pb.StartFRPC
}, nil }, nil
} }
client, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID) cli, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client := cli.ClientEntity
client.Stopped = false client.Stopped = false
if err = dao.NewQuery(ctx).UpdateClient(userInfo, client); err != nil { if err = dao.NewQuery(ctx).UpdateClient(userInfo, client); err != nil {

View File

@@ -29,11 +29,13 @@ func StopFRPCHandler(ctx *app.Context, req *pb.StopFRPCRequest) (*pb.StopFRPCRes
}, nil }, nil
} }
client, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID) cli, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client := cli.ClientEntity
client.Stopped = true client.Stopped = true
if err = dao.NewQuery(ctx).UpdateClient(userInfo, client); err != nil { if err = dao.NewQuery(ctx).UpdateClient(userInfo, client); err != nil {

View File

@@ -36,7 +36,7 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
}, err }, err
} }
cli, err := dao.NewQuery(c).GetClientByClientID(userInfo, reqClientID) cliRecord, err := dao.NewQuery(c).GetClientByClientID(userInfo, reqClientID)
if err != nil { if err != nil {
logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", reqClientID) logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", reqClientID)
return &pb.UpdateFRPCResponse{ return &pb.UpdateFRPCResponse{
@@ -44,6 +44,8 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
}, fmt.Errorf("cannot get client") }, fmt.Errorf("cannot get client")
} }
cli := cliRecord.ClientEntity
if cli.IsShadow { if cli.IsShadow {
cli, err = ChildClientForServer(c, serverID, cli) cli, err = ChildClientForServer(c, serverID, cli)
if err != nil { if err != nil {

View File

@@ -11,6 +11,7 @@ import (
"github.com/VaalaCat/frp-panel/biz/master/shell" "github.com/VaalaCat/frp-panel/biz/master/shell"
"github.com/VaalaCat/frp-panel/biz/master/streamlog" "github.com/VaalaCat/frp-panel/biz/master/streamlog"
"github.com/VaalaCat/frp-panel/biz/master/user" "github.com/VaalaCat/frp-panel/biz/master/user"
"github.com/VaalaCat/frp-panel/biz/master/worker"
"github.com/VaalaCat/frp-panel/middleware" "github.com/VaalaCat/frp-panel/middleware"
"github.com/VaalaCat/frp-panel/services/app" "github.com/VaalaCat/frp-panel/services/app"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -51,6 +52,7 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
clientRouter.POST("/init", app.Wrapper(appInstance, client.InitClientHandler)) clientRouter.POST("/init", app.Wrapper(appInstance, client.InitClientHandler))
clientRouter.POST("/delete", app.Wrapper(appInstance, client.DeleteClientHandler)) clientRouter.POST("/delete", app.Wrapper(appInstance, client.DeleteClientHandler))
clientRouter.POST("/list", app.Wrapper(appInstance, client.ListClientsHandler)) clientRouter.POST("/list", app.Wrapper(appInstance, client.ListClientsHandler))
clientRouter.POST("/install_workerd", app.Wrapper(appInstance, worker.InstallWorkerd))
} }
serverRouter := v1.Group("/server") serverRouter := v1.Group("/server")
{ {
@@ -83,6 +85,17 @@ func ConfigureRouter(appInstance app.Application, router *gin.Engine) {
proxyRouter.POST("/start_proxy", app.Wrapper(appInstance, proxy.StartProxy)) proxyRouter.POST("/start_proxy", app.Wrapper(appInstance, proxy.StartProxy))
proxyRouter.POST("/stop_proxy", app.Wrapper(appInstance, proxy.StopProxy)) proxyRouter.POST("/stop_proxy", app.Wrapper(appInstance, proxy.StopProxy))
} }
workerHandler := v1.Group("/worker")
{
workerHandler.POST("/get", app.Wrapper(appInstance, worker.GetWorker))
workerHandler.POST("/status", app.Wrapper(appInstance, worker.GetWorkerStatus))
workerHandler.POST("/create", app.Wrapper(appInstance, worker.CreateWorker))
workerHandler.POST("/list", app.Wrapper(appInstance, worker.ListWorkers))
workerHandler.POST("/remove", app.Wrapper(appInstance, worker.RemoveWorker))
workerHandler.POST("/update", app.Wrapper(appInstance, worker.UpdateWorker))
workerHandler.POST("/create_ingress", app.Wrapper(appInstance, worker.CreateWorkerIngress))
workerHandler.POST("/get_ingress", app.Wrapper(appInstance, worker.GetWorkerIngress))
}
v1.GET("/pty/:clientID", shell.PTYHandler(appInstance)) v1.GET("/pty/:clientID", shell.PTYHandler(appInstance))
v1.GET("/log", streamlog.GetLogHandler(appInstance)) v1.GET("/log", streamlog.GetLogHandler(appInstance))
} }

View File

@@ -30,7 +30,7 @@ func CreateProxyConfig(c *app.Context, req *pb.CreateProxyConfigRequest) (*pb.Cr
serverID = req.GetServerId() serverID = req.GetServerId()
) )
clientEntity, err := getClientWithMakeShadow(c, clientID, serverID) clientEntity, err := GetClientWithMakeShadow(c, clientID, serverID)
if err != nil { if err != nil {
logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
return nil, err return nil, err
@@ -42,13 +42,6 @@ func CreateProxyConfig(c *app.Context, req *pb.CreateProxyConfigRequest) (*pb.Cr
return nil, err return nil, err
} }
proxyCfg := &models.ProxyConfigEntity{}
if err := proxyCfg.FillClientConfig(clientEntity); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot fill client config, id: [%s]", clientID)
return nil, err
}
typedProxyCfgs, err := utils.LoadProxiesFromContent(req.GetConfig()) typedProxyCfgs, err := utils.LoadProxiesFromContent(req.GetConfig())
if err != nil { if err != nil {
logger.Logger(c).WithError(err).Errorf("cannot load proxies from content") logger.Logger(c).WithError(err).Errorf("cannot load proxies from content")
@@ -59,43 +52,86 @@ func CreateProxyConfig(c *app.Context, req *pb.CreateProxyConfigRequest) (*pb.Cr
return nil, fmt.Errorf("invalid config") return nil, fmt.Errorf("invalid config")
} }
if err := proxyCfg.FillTypedProxyConfig(typedProxyCfgs[0]); err != nil { if err := CreateProxyConfigWithTypedConfig(c, CreateProxyConfigWithTypedConfigParam{
logger.Logger(c).WithError(err).Errorf("cannot fill typed proxy config") ClientID: clientID,
ServerID: serverID,
ProxyCfg: typedProxyCfgs[0],
ClientEntity: clientEntity,
Overwrite: req.GetOverwrite(),
}); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot create proxy config")
return nil, err return nil, err
} }
return &pb.CreateProxyConfigResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
}
type CreateProxyConfigWithTypedConfigParam struct {
ClientID string
ServerID string
ProxyCfg v1.TypedProxyConfig
ClientEntity *models.ClientEntity
Overwrite bool
WorkerID *string
}
func CreateProxyConfigWithTypedConfig(c *app.Context, param CreateProxyConfigWithTypedConfigParam) error {
var (
userInfo = common.GetUserInfo(c)
clientID = param.ClientID
serverID = param.ServerID
clientEntity = param.ClientEntity
typedProxyCfg = param.ProxyCfg
err error
overwrite = param.Overwrite
)
proxyCfg := &models.ProxyConfigEntity{}
if err := proxyCfg.FillClientConfig(clientEntity); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot fill client config, id: [%s]", clientID)
return err
}
if err := proxyCfg.FillTypedProxyConfig(typedProxyCfg); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot fill typed proxy config")
return err
}
var existedProxyCfg *models.ProxyConfig var existedProxyCfg *models.ProxyConfig
existedProxyCfg, err = dao.NewQuery(c).GetProxyConfigByOriginClientIDAndName(userInfo, clientID, proxyCfg.Name) existedProxyCfg, err = dao.NewQuery(c).GetProxyConfigByOriginClientIDAndName(userInfo, clientID, proxyCfg.Name)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
logger.Logger(c).WithError(err).Errorf("cannot get proxy config, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot get proxy config, id: [%s]", clientID)
return nil, err return err
} }
if !req.GetOverwrite() && err == nil { if !overwrite && err == nil {
logger.Logger(c).Errorf("proxy config already exist, cfg: [%+v]", proxyCfg) logger.Logger(c).Errorf("proxy config already exist, cfg: [%+v]", proxyCfg)
return nil, fmt.Errorf("proxy config already exist") return fmt.Errorf("proxy config already exist")
} }
// update client config // update client config
if oldCfg, err := clientEntity.GetConfigContent(); err != nil { if oldCfg, err := clientEntity.GetConfigContent(); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot get client config, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot get client config, id: [%s]", clientID)
return nil, err return err
} else { } else {
oldCfg.Proxies = lo.Filter(oldCfg.Proxies, func(proxy v1.TypedProxyConfig, _ int) bool { 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 { if err := clientEntity.SetConfigContent(*oldCfg); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot set client config, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot set client config, id: [%s]", clientID)
return nil, err return err
} }
} }
rawCfg, err := clientEntity.MarshalJSONConfig() rawCfg, err := clientEntity.MarshalJSONConfig()
if err != nil { if err != nil {
logger.Logger(c).WithError(err).Errorf("cannot marshal client config, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot marshal client config, id: [%s]", clientID)
return nil, err return err
} }
_, err = client.UpdateFrpcHander(c, &pb.UpdateFRPCRequest{ _, err = client.UpdateFrpcHander(c, &pb.UpdateFRPCRequest{
@@ -117,11 +153,9 @@ func CreateProxyConfig(c *app.Context, req *pb.CreateProxyConfigRequest) (*pb.Cr
Name: &proxyCfg.Name, Name: &proxyCfg.Name,
}); err != nil { }); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot delete old proxy, client: [%s], server: [%s], proxy: [%s]", clientID, clientEntity.ServerID, proxyCfg.Name) logger.Logger(c).WithError(err).Errorf("cannot delete old proxy, client: [%s], server: [%s], proxy: [%s]", clientID, clientEntity.ServerID, proxyCfg.Name)
return nil, err return err
} }
} }
return &pb.CreateProxyConfigResponse{ return nil
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
} }

View File

@@ -49,7 +49,7 @@ func DeleteProxyConfig(c *app.Context, req *pb.DeleteProxyConfigRequest) (*pb.De
return nil, err return nil, err
} }
if err := dao.NewQuery(c).UpdateClient(userInfo, cli); err != nil { if err := dao.NewQuery(c).UpdateClient(userInfo, cli.ClientEntity); err != nil {
logger.Logger(c).WithError(err).Errorf("cannot update client, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot update client, id: [%s]", clientID)
return nil, err return nil, err
} }

View File

@@ -29,11 +29,11 @@ func convertProxyStatsList(proxyList []*models.ProxyStatsEntity) []*pb.ProxyInfo
}) })
} }
// getClientWithMakeShadow // GetClientWithMakeShadow
// 1. 检查是否有已连接该服务端的客户端 // 1. 检查是否有已连接该服务端的客户端
// 2. 检查是否有Shadow客户端 // 2. 检查是否有Shadow客户端
// 3. 如果没有则新建Shadow客户端和子客户端 // 3. 如果没有则新建Shadow客户端和子客户端
func getClientWithMakeShadow(c *app.Context, clientID, serverID string) (*models.ClientEntity, error) { func GetClientWithMakeShadow(c *app.Context, clientID, serverID string) (*models.ClientEntity, error) {
userInfo := common.GetUserInfo(c) userInfo := common.GetUserInfo(c)
clientEntity, err := dao.NewQuery(c).GetClientByFilter(userInfo, &models.ClientEntity{OriginClientID: clientID, ServerID: serverID}, lo.ToPtr(false)) clientEntity, err := dao.NewQuery(c).GetClientByFilter(userInfo, &models.ClientEntity{OriginClientID: clientID, ServerID: serverID}, lo.ToPtr(false))
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {

View File

@@ -21,7 +21,7 @@ func StartProxy(ctx *app.Context, req *pb.StartProxyRequest) (*pb.StartProxyResp
proxyName = req.GetName() proxyName = req.GetName()
) )
clientEntity, err := getClientWithMakeShadow(ctx, clientID, serverID) clientEntity, err := GetClientWithMakeShadow(ctx, clientID, serverID)
if err != nil { if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", clientID) logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
return nil, err return nil, err

View File

@@ -20,7 +20,7 @@ func StopProxy(ctx *app.Context, req *pb.StopProxyRequest) (*pb.StopProxyRespons
proxyName = req.GetName() proxyName = req.GetName()
) )
clientEntity, err := getClientWithMakeShadow(ctx, clientID, serverID) clientEntity, err := GetClientWithMakeShadow(ctx, clientID, serverID)
if err != nil { if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", clientID) logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
return nil, err return nil, err

View File

@@ -26,12 +26,14 @@ func UpdateProxyConfig(c *app.Context, req *pb.UpdateProxyConfigRequest) (*pb.Up
serverID = req.GetServerId() serverID = req.GetServerId()
) )
clientEntity, err := dao.NewQuery(c).GetClientByClientID(userInfo, clientID) cli, err := dao.NewQuery(c).GetClientByClientID(userInfo, clientID)
if err != nil { if err != nil {
logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
return nil, err return nil, err
} }
clientEntity := cli.ClientEntity
if clientEntity.ServerID != serverID { if clientEntity.ServerID != serverID {
logger.Logger(c).Errorf("client and server not match, find or create client, client: [%s], server: [%s]", clientID, serverID) logger.Logger(c).Errorf("client and server not match, find or create client, client: [%s], server: [%s]", clientID, serverID)
originClient, err := dao.NewQuery(c).GetClientByClientID(userInfo, clientEntity.OriginClientID) originClient, err := dao.NewQuery(c).GetClientByClientID(userInfo, clientEntity.OriginClientID)
@@ -40,7 +42,7 @@ func UpdateProxyConfig(c *app.Context, req *pb.UpdateProxyConfigRequest) (*pb.Up
return nil, err return nil, err
} }
clientEntity, err = client.ChildClientForServer(c, serverID, originClient) clientEntity, err = client.ChildClientForServer(c, serverID, originClient.ClientEntity)
if err != nil { if err != nil {
logger.Logger(c).WithError(err).Errorf("cannot create child client, id: [%s]", clientID) logger.Logger(c).WithError(err).Errorf("cannot create child client, id: [%s]", clientID)
return nil, err return nil, err

View File

@@ -0,0 +1,72 @@
package worker
import (
"fmt"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/services/workerd"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func CreateWorker(ctx *app.Context, req *pb.CreateWorkerRequest) (*pb.CreateWorkerResponse, error) {
var (
userInfo = common.GetUserInfo(ctx)
clientId = req.GetClientId()
reqWorker = req.GetWorker()
)
if err := validateCreateWorker(req); err != nil {
logger.Logger(ctx).WithError(err).Errorf("invalid create worker request, origin is: [%s]", req.String())
return nil, err
}
cli, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s], workerName: [%s]", clientId, reqWorker.GetName())
return nil, err
}
workerd.FillWorkerValue(reqWorker, uint(userInfo.GetUserID()))
workerToCreate := (&models.Worker{}).FromPB(reqWorker)
workerToCreate.WorkerModel = nil
workerToCreate.Clients = append(workerToCreate.Clients, *cli)
if err := dao.NewQuery(ctx).CreateWorker(userInfo, workerToCreate); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot create worker, workerName: [%s]", workerToCreate.Name)
return nil, err
}
go func() {
bgCtx := ctx.Background()
resp := &pb.CreateWorkerResponse{}
err := rpc.CallClientWrapper(bgCtx, clientId, pb.Event_EVENT_CREATE_WORKER, req, resp)
if err != nil {
logger.Logger(bgCtx).WithError(err).Errorf("create worker event send to client error, client id: [%s], worker name: [%s]", clientId, workerToCreate.Name)
}
}()
logger.Logger(ctx).Infof("create worker success, workerName: [%s], start to create worker's proxy", workerToCreate.Name)
return &pb.CreateWorkerResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
WorkerId: &workerToCreate.ID,
}, nil
}
func validateCreateWorker(req *pb.CreateWorkerRequest) error {
if len(req.GetClientId()) == 0 {
return fmt.Errorf("invalid client id")
}
if req.GetWorker() == nil {
return fmt.Errorf("invalid worker")
}
return nil
}

View File

@@ -0,0 +1,106 @@
package worker
import (
"fmt"
"strings"
"github.com/VaalaCat/frp-panel/biz/master/proxy"
"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"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils/logger"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
func IngressName(worker *models.Worker, cli *models.ClientEntity) string {
return fmt.Sprintf("ingress-%s-%s", strings.Split(worker.ID, "-")[0], cli.OriginClientID)
}
func CreateWorkerIngress(ctx *app.Context, req *pb.CreateWorkerIngressRequest) (*pb.CreateWorkerIngressResponse, error) {
if err := validateCreateWorkerIngressRequest(req); err != nil {
logger.Logger(ctx).WithError(err).Errorf("invalid create worker ingress request, origin is: [%s]", req.String())
return nil, err
}
var (
clientId = req.GetClientId()
serverId = req.GetServerId()
workerId = req.GetWorkerId()
userInfo = common.GetUserInfo(ctx)
)
clientEntity, err := proxy.GetClientWithMakeShadow(ctx, clientId, serverId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", clientId)
return nil, err
}
_, err = dao.NewQuery(ctx).GetServerByServerID(userInfo, serverId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get server, id: [%s]", serverId)
return nil, err
}
workerToExpose, err := dao.NewQuery(ctx).GetWorkerByWorkerID(userInfo, workerId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get worker, id: [%s]", workerId)
return nil, err
}
httpProxyCfg := v1.HTTPProxyConfig{
ProxyBaseConfig: v1.ProxyBaseConfig{
Name: IngressName(workerToExpose, clientEntity),
Type: string(v1.ProxyTypeHTTP),
Annotations: map[string]string{
defs.FrpProxyAnnotationsKey_Ingress: "true",
defs.FrpProxyAnnotationsKey_WorkerId: workerId,
},
ProxyBackend: v1.ProxyBackend{
Plugin: v1.TypedClientPluginOptions{
Type: v1.PluginUnixDomainSocket,
ClientPluginOptions: &v1.UnixDomainSocketPluginOptions{
Type: v1.PluginUnixDomainSocket,
UnixPath: fmt.Sprintf("@%s", strings.TrimPrefix(workerToExpose.Socket.Data.GetAddress(), "unix-abstract:")),
},
},
},
},
DomainConfig: v1.DomainConfig{
SubDomain: workerId,
},
}
if err := proxy.CreateProxyConfigWithTypedConfig(ctx, proxy.CreateProxyConfigWithTypedConfigParam{
ClientID: clientId,
ServerID: serverId,
ProxyCfg: v1.TypedProxyConfig{
Type: string(v1.ProxyTypeHTTP),
ProxyConfigurer: &httpProxyCfg,
},
ClientEntity: clientEntity,
Overwrite: true,
}); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot create proxy config, client id: [%s], server id: [%s], worker id: [%s]", clientId, serverId, workerId)
return nil, err
}
return &pb.CreateWorkerIngressResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
}
func validateCreateWorkerIngressRequest(req *pb.CreateWorkerIngressRequest) error {
if req == nil {
return fmt.Errorf("invalid request")
}
if len(req.GetClientId()) == 0 || len(req.GetServerId()) == 0 || len(req.GetWorkerId()) == 0 {
return fmt.Errorf("invalid request, client id: [%s], server id: [%s], worker id: [%s]", req.GetClientId(), req.GetServerId(), req.GetWorkerId())
}
return nil
}

View File

@@ -0,0 +1,46 @@
package worker
import (
"fmt"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
)
func GetWorker(ctx *app.Context, req *pb.GetWorkerRequest) (*pb.GetWorkerResponse, error) {
logger.Logger(ctx).Infof("get worker req: %s", req.String())
var (
workerID = req.GetWorkerId()
userInfo = common.GetUserInfo(ctx)
)
if len(workerID) == 0 {
logger.Logger(ctx).Errorf("worker id is empty")
return nil, fmt.Errorf("worker id is empty")
}
workerRecord, err := dao.NewQuery(ctx).GetWorkerByWorkerID(userInfo, workerID)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("get worker by id failed")
return nil, err
}
return &pb.GetWorkerResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "ok",
},
Worker: workerRecord.ToPB(),
Clients: lo.Map(workerRecord.Clients, func(client models.Client, index int) *pb.Client {
c := client.ToPB()
c.Config = nil
c.Secret = nil
return c
}),
}, nil
}

View File

@@ -0,0 +1,32 @@
package worker
import (
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
)
func GetWorkerIngress(ctx *app.Context, req *pb.GetWorkerIngressRequest) (*pb.GetWorkerIngressResponse, error) {
logger.Logger(ctx).Infof("get worker: [%s] ingress", req.GetWorkerId())
var (
workerId = req.GetWorkerId()
userInfo = common.GetUserInfo(ctx)
)
proxyCfgs, err := dao.NewQuery(ctx).GetProxyConfigsByWorkerId(userInfo, workerId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("failed to get proxy configs for worker: [%s]", workerId)
return nil, err
}
logger.Logger(ctx).Infof("got proxy configs for worker: [%s] success, ingresses length: [%d]", workerId, len(proxyCfgs))
return &pb.GetWorkerIngressResponse{
ProxyConfigs: lo.Map(proxyCfgs, func(item *models.ProxyConfig, index int) *pb.ProxyConfig {
return item.ToPB()
}),
}, nil
}

View File

@@ -0,0 +1,65 @@
package worker
import (
"fmt"
"maps"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
"github.com/sourcegraph/conc/pool"
)
func GetWorkerStatus(ctx *app.Context, req *pb.GetWorkerStatusRequest) (*pb.GetWorkerStatusResponse, error) {
var (
workerID = req.GetWorkerId()
userInfo = common.GetUserInfo(ctx)
)
if len(workerID) == 0 {
logger.Logger(ctx).Errorf("worker id is empty")
return nil, fmt.Errorf("worker id is empty")
}
workerRecord, err := dao.NewQuery(ctx).GetWorkerByWorkerID(userInfo, workerID)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("get worker by id failed")
return nil, err
}
clientIds := lo.Map(workerRecord.Clients, func(cli models.Client, _ int) string {
return cli.ClientID
})
var pool pool.ResultErrorPool[*pb.GetWorkerStatusResponse]
for _, clientID := range clientIds {
pool.Go(func() (*pb.GetWorkerStatusResponse, error) {
bgCtx := ctx.Background()
cliResp := &pb.GetWorkerStatusResponse{}
err := rpc.CallClientWrapper(bgCtx, clientID, pb.Event_EVENT_GET_WORKER_STATUS, &pb.GetWorkerStatusRequest{}, cliResp)
return cliResp, err
})
}
resps, err := pool.Wait()
if err != nil {
logger.Logger(ctx).WithError(err).Warnf("get worker status failed")
}
statusMap := map[string]string{}
for _, r := range resps {
s := r.GetWorkerStatus()
maps.Copy(statusMap, s)
}
return &pb.GetWorkerStatusResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
WorkerStatus: statusMap,
}, nil
}

View File

@@ -0,0 +1,38 @@
package worker
import (
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/utils/logger"
)
func InstallWorkerd(ctx *app.Context, req *pb.InstallWorkerdRequest) (*pb.InstallWorkerdResponse, error) {
var (
userInfo = common.GetUserInfo(ctx)
clientId = req.GetClientId()
)
logger.Logger(ctx).Infof("installw orkerd called with userInfo: %v, clientId: %s", userInfo, clientId)
_, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("failed to get client by clientID: %s", clientId)
return nil, err
}
resp := &pb.InstallWorkerdResponse{}
if err := rpc.CallClientWrapper(ctx, clientId, pb.Event_EVENT_INSTALL_WORKERD, req, resp); err != nil {
logger.Logger(ctx).WithError(err).Errorf("failed to call install workerd with clientId: %s", clientId)
return nil, err
}
logger.Logger(ctx).Infof("install workerd success with clientId: %s", clientId)
return &pb.InstallWorkerdResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "ok",
},
}, nil
}

View File

@@ -0,0 +1,100 @@
package worker
import (
"fmt"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
)
func ListWorkers(ctx *app.Context, req *pb.ListWorkersRequest) (*pb.ListWorkersResponse, error) {
var (
userInfo = common.GetUserInfo(ctx)
page = int(req.GetPage())
pageSize = int(req.GetPageSize())
keyword = req.GetKeyword()
err error
workers []*models.Worker
workerCounts int64
hasKeyword = len(keyword) > 0
)
if page == 0 {
page = 1
}
if pageSize == 0 {
pageSize = 10
}
if hasKeyword {
workers, err = dao.NewQuery(ctx).ListWorkersWithKeyword(userInfo, page, pageSize, keyword)
} else {
workers, err = dao.NewQuery(ctx).ListWorkers(userInfo, page, pageSize)
}
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot list workers, page: [%d], pageSize: [%d], keyword: [%s]", page, pageSize, keyword)
return &pb.ListWorkersResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_NOT_FOUND, Message: err.Error()},
}, fmt.Errorf("cannot list workers, page: [%d], pageSize: [%d], keyword: [%s]", page, pageSize, keyword)
}
if hasKeyword {
workerCounts, err = dao.NewQuery(ctx).CountWorkersWithKeyword(userInfo, keyword)
} else {
workerCounts, err = dao.NewQuery(ctx).CountWorkers(userInfo)
}
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot count workers, keyword: [%s]", keyword)
return &pb.ListWorkersResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_NOT_FOUND, Message: err.Error()},
}, fmt.Errorf("cannot count workers, keyword: [%s]", keyword)
}
return &pb.ListWorkersResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "success",
},
Total: lo.ToPtr(int32(workerCounts)),
Workers: lo.Map(workers, func(w *models.Worker, _ int) *pb.Worker {
k := w.ToPB()
k.Code = nil
k.ConfigTemplate = nil
return k
}),
}, nil
}
func ListClientWorkers(ctx *app.Context, req *pb.ListClientWorkersRequest) (*pb.ListClientWorkersResponse, error) {
var (
err error
workers []*models.Worker
clientId = req.GetBase().GetClientId()
)
workers, err = dao.NewQuery(ctx).AdminListWorkersByClientID(clientId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot list workers, clientId: [%s]", clientId)
return &pb.ListClientWorkersResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_NOT_FOUND, Message: err.Error()},
}, fmt.Errorf("cannot list workers, clientId: [%s]", clientId)
}
logger.Logger(ctx).Infof("list workers, clientId: [%s], worker len: [%d]", clientId, len(workers))
return &pb.ListClientWorkersResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "success",
},
Workers: lo.Map(workers, func(w *models.Worker, _ int) *pb.Worker {
k := w.ToPB()
return k
}),
}, nil
}

View File

@@ -0,0 +1,75 @@
package worker
import (
"github.com/VaalaCat/frp-panel/biz/master/proxy"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
)
func RemoveWorker(ctx *app.Context, req *pb.RemoveWorkerRequest) (*pb.RemoveWorkerResponse, error) {
var (
userInfo = common.GetUserInfo(ctx)
workerId = req.GetWorkerId()
)
logger.Logger(ctx).Infof("start remove worker, id: [%s]", workerId)
workerToDelete, err := dao.NewQuery(ctx).GetWorkerByWorkerID(userInfo, workerId)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get worker, id: [%s]", workerId)
return nil, err
}
if ingressesToDelete, err := dao.NewQuery(ctx).GetProxyConfigsByWorkerId(userInfo, workerId); err == nil {
for _, ingressToDelete := range ingressesToDelete {
logger.Logger(ctx).Infof("start to remove worker ingress on server: [%s] client: [%s], name: [%s]", ingressToDelete.ServerID, ingressToDelete.ClientID, ingressToDelete.Name)
resp, err := proxy.DeleteProxyConfig(ctx, &pb.DeleteProxyConfigRequest{
ServerId: lo.ToPtr(ingressToDelete.ServerID),
ClientId: lo.ToPtr(ingressToDelete.ClientID),
Name: lo.ToPtr(ingressToDelete.Name),
})
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot remove worker ingress on server: [%s] client: [%s], name: [%s], resp is: [%s]",
ingressToDelete.ServerID, ingressToDelete.ClientID, ingressToDelete.Name, resp.String())
return nil, err
}
logger.Logger(ctx).Infof("remove worker ingress success on server: [%s] client: [%s], name: [%s]", ingressToDelete.ServerID, ingressToDelete.ClientID, ingressToDelete.Name)
}
}
if err := dao.NewQuery(ctx).DeleteWorker(userInfo, workerId); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot remove worker, id: [%s]", workerId)
return nil, err
}
go func() {
bgCtx := ctx.Background()
hasErr := false
for _, cli := range workerToDelete.Clients {
resp := &pb.RemoveWorkerResponse{}
if err := rpc.CallClientWrapper(bgCtx, cli.ClientID, pb.Event_EVENT_REMOVE_WORKER, req, resp); err != nil {
logger.Logger(bgCtx).WithError(err).Errorf("remove event send to client error, client id: [%s]", cli.ClientID)
hasErr = true
continue
}
}
if hasErr {
logger.Logger(bgCtx).Errorf("remove event send to client error")
}
}()
logger.Logger(ctx).Infof("remove worker success, id: [%s]", workerId)
return &pb.RemoveWorkerResponse{
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
}, nil
}

View File

@@ -0,0 +1 @@
package worker

View File

@@ -0,0 +1 @@
package worker

View File

@@ -0,0 +1,104 @@
package worker
import (
"fmt"
"github.com/VaalaCat/frp-panel/common"
"github.com/VaalaCat/frp-panel/models"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/services/dao"
"github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
)
func UpdateWorker(ctx *app.Context, req *pb.UpdateWorkerRequest) (*pb.UpdateWorkerResponse, error) {
var (
clientIds = req.GetClientIds()
wrokerReq = req.GetWorker()
userInfo = common.GetUserInfo(ctx)
oldClientIds []string
)
workerToUpdate, err := dao.NewQuery(ctx).GetWorkerByWorkerID(userInfo, wrokerReq.GetWorkerId())
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get worker, id: [%s]", wrokerReq.GetWorkerId())
return nil, fmt.Errorf("cannot get worker, id: [%s]", wrokerReq.GetWorkerId())
}
clis, err := dao.NewQuery(ctx).GetClientsByClientIDs(userInfo, clientIds)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", utils.MarshalForJson(clientIds))
return nil, fmt.Errorf("cannot get client, id: [%s]", utils.MarshalForJson(clientIds))
}
updatedFields := []string{}
if len(clientIds) != 0 {
oldClientIds = lo.Map(workerToUpdate.Clients, func(c models.Client, _ int) string { return c.ClientID })
workerToUpdate.Clients = lo.Map(clis, func(c *models.Client, _ int) models.Client { return *c })
updatedFields = append(updatedFields, "client_id")
} else {
oldClientIds = lo.Map(workerToUpdate.Clients, func(c models.Client, _ int) string { return c.ClientID })
}
if len(wrokerReq.GetName()) != 0 {
workerToUpdate.Name = wrokerReq.GetName()
updatedFields = append(updatedFields, "name")
}
if len(wrokerReq.GetCode()) != 0 {
workerToUpdate.Code = wrokerReq.GetCode()
updatedFields = append(updatedFields, "code")
}
if len(wrokerReq.GetConfigTemplate()) != 0 {
workerToUpdate.ConfigTemplate = wrokerReq.GetConfigTemplate()
updatedFields = append(updatedFields, "config_template")
}
if err := dao.NewQuery(ctx).UpdateWorker(userInfo, workerToUpdate); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot update worker, id: [%s]", wrokerReq.GetWorkerId())
return nil, fmt.Errorf("cannot update worker, id: [%s]", wrokerReq.GetWorkerId())
}
go func() {
bgCtx := ctx.Background()
for _, oldClientId := range oldClientIds {
removeResp := &pb.RemoveWorkerResponse{}
err := rpc.CallClientWrapper(bgCtx, oldClientId, pb.Event_EVENT_REMOVE_WORKER, &pb.RemoveWorkerRequest{
ClientId: &oldClientId,
WorkerId: &workerToUpdate.ID,
}, removeResp)
if err != nil {
logger.Logger(bgCtx).WithError(err).Errorf("remove old worker event send to client error, clients: [%s], worker name: [%s]", oldClientId, workerToUpdate.Name)
}
}
for _, newClient := range clis {
createResp := &pb.CreateWorkerResponse{}
err = rpc.CallClientWrapper(bgCtx, newClient.ClientID, pb.Event_EVENT_CREATE_WORKER, &pb.CreateWorkerRequest{
ClientId: &newClient.ClientID,
Worker: workerToUpdate.ToPB(),
}, createResp)
if err != nil {
logger.Logger(bgCtx).WithError(err).Errorf("update new worker event send to client error, client id: [%s], worker name: [%s]", newClient.ClientID, workerToUpdate.Name)
}
}
logger.Logger(ctx).Infof("update worker event send to client success, clients: [%s], worker name: [%s], remove old worker send to those clients: %s",
utils.MarshalForJson(clis), workerToUpdate.Name, utils.MarshalForJson(oldClientIds))
}()
logger.Logger(ctx).Infof("update worker success, id: [%s], updated fields: %s", wrokerReq.GetWorkerId(), utils.MarshalForJson(updatedFields))
return &pb.UpdateWorkerResponse{
Status: &pb.Status{
Code: pb.RespCode_RESP_CODE_SUCCESS,
Message: "ok",
},
}, nil
}

View File

@@ -21,10 +21,11 @@ type runClientParam struct {
Lc fx.Lifecycle Lc fx.Lifecycle
Ctx *app.Context Ctx *app.Context
AppInstance app.Application AppInstance app.Application
TaskManager watcher.Client `name:"clientTaskManager"` TaskManager watcher.Client `name:"clientTaskManager"`
Cfg conf.Config WorkersManager app.WorkersManager
Cfg conf.Config
} }
func runClient(param runClientParam) { func runClient(param runClientParam) {
@@ -45,6 +46,8 @@ func runClient(param runClientParam) {
param.TaskManager.AddDurationTask(defs.PullConfigDuration, param.TaskManager.AddDurationTask(defs.PullConfigDuration,
bizclient.PullConfig, appInstance, clientID, clientSecret) bizclient.PullConfig, appInstance, clientID, clientSecret)
param.TaskManager.AddDurationTask(defs.PullClientWorkersDuration,
bizclient.PullWorkers, appInstance, clientID, clientSecret)
var wg conc.WaitGroup var wg conc.WaitGroup
param.Lc.Append(fx.Hook{ param.Lc.Append(fx.Hook{
@@ -62,7 +65,10 @@ func runClient(param runClientParam) {
) )
appInstance.SetClientRPCHandler(cliRpcHandler) appInstance.SetClientRPCHandler(cliRpcHandler)
// --- init once start ---
initClientOnce(appInstance, clientID, clientSecret) initClientOnce(appInstance, clientID, clientSecret)
initClientWorkerOnce(appInstance, clientID, clientSecret)
// --- init once stop ----
wg.Go(cliRpcHandler.Run) wg.Go(cliRpcHandler.Run)
wg.Go(param.TaskManager.Run) wg.Go(param.TaskManager.Run)
@@ -84,3 +90,10 @@ func initClientOnce(appInstance app.Application, clientID, clientSecret string)
logger.Logger(context.Background()).WithError(err).Errorf("cannot pull client config, wait for retry") logger.Logger(context.Background()).WithError(err).Errorf("cannot pull client config, wait for retry")
} }
} }
func initClientWorkerOnce(appInstance app.Application, clientID, clientSecret string) {
err := bizclient.PullWorkers(appInstance, clientID, clientSecret)
if err != nil {
logger.Logger(context.Background()).WithError(err).Errorf("cannot pull client workers, wait for retry")
}
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/samber/lo"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"go.uber.org/fx" "go.uber.org/fx"
@@ -383,13 +384,13 @@ func patchConfig(appInstance app.Application, commonArgs CommonArgs) conf.Config
tmpCfg.Client.RPCUrl = *commonArgs.RpcUrl tmpCfg.Client.RPCUrl = *commonArgs.RpcUrl
} }
if commonArgs.RpcPort != nil || commonArgs.ApiPort != nil || if lo.FromPtrOr(commonArgs.RpcPort, 0) != 0 || lo.FromPtrOr(commonArgs.ApiPort, 0) != 0 ||
commonArgs.ApiScheme != nil || lo.FromPtrOr(commonArgs.ApiScheme, "") != "" ||
commonArgs.RpcHost != nil || commonArgs.ApiHost != nil { lo.FromPtrOr(commonArgs.RpcHost, "") != "" || lo.FromPtrOr(commonArgs.ApiHost, "") != "" {
logger.Logger(c).Warnf("deprecatedenv configs !!! pls use api url and rpc url \n\n rpc host: %s, rpc port: %d, api host: %s, api port: %d, api scheme: %s", logger.Logger(c).Warnf("deprecatedenv configs !!! pls use api url and rpc url \n\n rpc host: %s, rpc port: %d, api host: %s, api port: %d, api scheme: %s, \n\n args: %s",
tmpCfg.Master.RPCHost, tmpCfg.Master.RPCPort, tmpCfg.Master.RPCHost, tmpCfg.Master.RPCPort,
tmpCfg.Master.APIHost, tmpCfg.Master.APIPort, tmpCfg.Master.APIHost, tmpCfg.Master.APIPort,
tmpCfg.Master.APIScheme) tmpCfg.Master.APIScheme, utils.MarshalForJson(tmpCfg))
} else if len(tmpCfg.Client.APIUrl) > 0 || len(tmpCfg.Client.RPCUrl) > 0 { } else if len(tmpCfg.Client.APIUrl) > 0 || len(tmpCfg.Client.RPCUrl) > 0 {
logger.Logger(c).Infof("env config, api url: %s, rpc url: %s", tmpCfg.Client.APIUrl, tmpCfg.Client.RPCUrl) logger.Logger(c).Infof("env config, api url: %s, rpc url: %s", tmpCfg.Client.APIUrl, tmpCfg.Client.RPCUrl)
} }

View File

@@ -7,6 +7,8 @@ import (
var ( var (
clientMod = fx.Module("cmd.client", clientMod = fx.Module("cmd.client",
fx.Provide( fx.Provide(
NewWorkerExecManager,
NewWorkersManager,
fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)), fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)),
)) ))

View File

@@ -6,6 +6,7 @@ import (
"embed" "embed"
"net" "net"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"sync" "sync"
@@ -26,6 +27,7 @@ import (
"github.com/VaalaCat/frp-panel/services/rbac" "github.com/VaalaCat/frp-panel/services/rbac"
"github.com/VaalaCat/frp-panel/services/rpc" "github.com/VaalaCat/frp-panel/services/rpc"
"github.com/VaalaCat/frp-panel/services/watcher" "github.com/VaalaCat/frp-panel/services/watcher"
"github.com/VaalaCat/frp-panel/services/workerd"
"github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/VaalaCat/frp-panel/utils/wsgrpc" "github.com/VaalaCat/frp-panel/utils/wsgrpc"
@@ -373,3 +375,39 @@ func NewEnforcer(param struct {
param.AppInstance.SetEnforcer(e) param.AppInstance.SetEnforcer(e)
return e return e
} }
func NewWorkersManager(lx fx.Lifecycle, mgr app.WorkerExecManager, appInstance app.Application) app.WorkersManager {
if !appInstance.GetConfig().Client.Features.EnableFunctions {
return nil
}
workerMgr := workerd.NewWorkersManager()
appInstance.SetWorkersManager(workerMgr)
lx.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
workerMgr.StopAll()
logger.Logger(ctx).Info("stop all workers")
return nil
},
})
return workerMgr
}
func NewWorkerExecManager(cfg conf.Config, appInstance app.Application) app.WorkerExecManager {
if !appInstance.GetConfig().Client.Features.EnableFunctions {
return nil
}
workerdBinPath := cfg.Client.Worker.WorkerdBinaryPath
if err := os.MkdirAll(cfg.Client.Worker.WorkerdWorkDir, os.ModePerm); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("create work dir failed, path: [%s]", cfg.Client.Worker.WorkerdWorkDir)
}
mgr := workerd.NewExecManager(workerdBinPath,
[]string{"serve", "--watch", "--verbose"})
appInstance.SetWorkerExecManager(mgr)
return mgr
}

View File

@@ -25,7 +25,10 @@ type ReqType interface {
pb.GetProxyStatsByClientIDRequest | pb.GetProxyStatsByServerIDRequest | pb.GetProxyStatsByClientIDRequest | pb.GetProxyStatsByServerIDRequest |
pb.CreateProxyConfigRequest | pb.ListProxyConfigsRequest | pb.UpdateProxyConfigRequest | pb.CreateProxyConfigRequest | pb.ListProxyConfigsRequest | pb.UpdateProxyConfigRequest |
pb.DeleteProxyConfigRequest | pb.GetProxyConfigRequest | pb.SignTokenRequest | pb.DeleteProxyConfigRequest | pb.GetProxyConfigRequest | pb.SignTokenRequest |
pb.StartProxyRequest | pb.StopProxyRequest pb.StartProxyRequest | pb.StopProxyRequest |
pb.CreateWorkerRequest | pb.RemoveWorkerRequest | pb.RunWorkerRequest | pb.StopWorkerRequest | pb.UpdateWorkerRequest | pb.GetWorkerRequest |
pb.ListWorkersRequest | pb.CreateWorkerIngressRequest | pb.GetWorkerIngressRequest |
pb.GetWorkerStatusRequest | pb.InstallWorkerdRequest
} }
func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) { func GetProtoRequest[T ReqType](c *gin.Context) (r *T, err error) {

View File

@@ -26,7 +26,10 @@ type RespType interface {
pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse | pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse |
pb.CreateProxyConfigResponse | pb.ListProxyConfigsResponse | pb.UpdateProxyConfigResponse | pb.CreateProxyConfigResponse | pb.ListProxyConfigsResponse | pb.UpdateProxyConfigResponse |
pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse | pb.SignTokenResponse | pb.DeleteProxyConfigResponse | pb.GetProxyConfigResponse | pb.SignTokenResponse |
pb.StartProxyResponse | pb.StopProxyResponse pb.StartProxyResponse | pb.StopProxyResponse |
pb.CreateWorkerResponse | pb.RemoveWorkerResponse | pb.RunWorkerResponse | pb.StopWorkerResponse | pb.UpdateWorkerResponse | pb.GetWorkerResponse |
pb.ListWorkersResponse | pb.CreateWorkerIngressResponse | pb.GetWorkerIngressResponse |
pb.GetWorkerStatusResponse | pb.InstallWorkerdResponse
} }
func OKResp[T RespType](c *gin.Context, origin *T) { func OKResp[T RespType](c *gin.Context, origin *T) {
@@ -96,6 +99,14 @@ func getEvent(origin interface{}) (pb.Event, protoreflect.ProtoMessage, error) {
return pb.Event_EVENT_STOP_FRPS, ptr, nil return pb.Event_EVENT_STOP_FRPS, ptr, nil
case *pb.GetProxyConfigResponse: case *pb.GetProxyConfigResponse:
return pb.Event_EVENT_GET_PROXY_INFO, ptr, nil return pb.Event_EVENT_GET_PROXY_INFO, ptr, nil
case *pb.CreateWorkerResponse:
return pb.Event_EVENT_CREATE_WORKER, ptr, nil
case *pb.RemoveWorkerResponse:
return pb.Event_EVENT_REMOVE_WORKER, ptr, nil
case *pb.GetWorkerStatusResponse:
return pb.Event_EVENT_GET_WORKER_STATUS, ptr, nil
case *pb.InstallWorkerdResponse:
return pb.Event_EVENT_INSTALL_WORKERD, ptr, nil
default: default:
return 0, nil, fmt.Errorf("cannot unmarshal unknown type: %T", origin) return 0, nil, fmt.Errorf("cannot unmarshal unknown type: %T", origin)
} }

View File

@@ -5,8 +5,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger" "github.com/VaalaCat/frp-panel/utils/logger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/ilyakaznacheev/cleanenv" "github.com/ilyakaznacheev/cleanenv"
@@ -52,12 +54,25 @@ type Config struct {
RPCUrl string `env:"RPC_URL" env-description:"rpc url, support ws or wss or grpc scheme, eg: ws://127.0.0.1:9000"` RPCUrl string `env:"RPC_URL" env-description:"rpc url, support ws or wss or grpc scheme, eg: ws://127.0.0.1:9000"`
APIUrl string `env:"API_URL" env-description:"api url, support http or https scheme, eg: http://127.0.0.1:9000"` APIUrl string `env:"API_URL" env-description:"api url, support http or https scheme, eg: http://127.0.0.1:9000"`
TLSInsecureSkipVerify bool `env:"TLS_INSECURE_SKIP_VERIFY" env-default:"true" env-description:"skip tls verify"` TLSInsecureSkipVerify bool `env:"TLS_INSECURE_SKIP_VERIFY" env-default:"true" env-description:"skip tls verify"`
Worker struct {
WorkerdBinaryPath string `env:"WORKERD_BINARY_PATH" env-description:"workerd binary path"`
WorkerdWorkDir string `env:"WORKERD_WORK_DIR" env-default:"/tmp/frpp/workerd" env-description:"workerd work dir"`
WorkerdDownloadURL struct {
UseProxy bool `env:"USE_PROXY" env-default:"true" env-description:"use proxy"`
LinuxArm64 string `env:"LINUX_ARM64" env-default:"https://github.com/cloudflare/workerd/releases/download/v1.20250505.0/workerd-linux-arm64.gz"`
LinuxX8664 string `env:"LINUX_X86_64" env-default:"https://github.com/cloudflare/workerd/releases/download/v1.20250505.0/workerd-linux-64.gz"`
} `env-prefix:"WORKERD_DOWNLOAD_URL_" env-description:"workerd download url"`
} `env-prefix:"WORKER_" env-description:"worker's config"`
Features struct {
EnableFunctions bool `env:"ENABLE_FUNCTIONS" env-default:"true" env-description:"enable functions"`
} `env-prefix:"FEATURES_" env-description:"features config"`
} `env-prefix:"CLIENT_"` } `env-prefix:"CLIENT_"`
IsDebug bool `env:"IS_DEBUG" env-default:"false" env-description:"is debug mode"` IsDebug bool `env:"IS_DEBUG" env-default:"false" env-description:"is debug mode"`
Logger struct { Logger struct {
DefaultLoggerLevel string `env:"DEFAULT_LOGGER_LEVEL" env-default:"info" env-description:"frp-panel internal default logger level"` DefaultLoggerLevel string `env:"DEFAULT_LOGGER_LEVEL" env-default:"info" env-description:"frp-panel internal default logger level"`
FRPLoggerLevel string `env:"FRP_LOGGER_LEVEL" env-default:"info" env-description:"frp logger level"` FRPLoggerLevel string `env:"FRP_LOGGER_LEVEL" env-default:"info" env-description:"frp logger level"`
} `env-prefix:"LOGGER_"` } `env-prefix:"LOGGER_"`
HTTP_PROXY string `env:"HTTP_PROXY" env-description:"http proxy"`
} }
func NewConfig() Config { func NewConfig() Config {
@@ -115,6 +130,21 @@ func (cfg *Config) Complete() {
if len(cfg.Client.ID) == 0 { if len(cfg.Client.ID) == 0 {
cfg.Client.ID = hostname cfg.Client.ID = hostname
} }
cwd, err := os.Getwd()
if err != nil {
fmt.Println("failed to get current working directory:", err)
os.Exit(1)
}
if len(cfg.Client.Worker.WorkerdBinaryPath) == 0 {
w, _ := utils.FindExecutableNames(func(name string) bool {
return strings.HasPrefix(name, "workerd")
}, cwd, "/")
if len(w) > 0 {
cfg.Client.Worker.WorkerdBinaryPath = w[0]
}
}
} }
func (cfg Config) PrintStr() string { func (cfg Config) PrintStr() string {

View File

@@ -68,8 +68,9 @@ const (
) )
const ( const (
PullConfigDuration = 30 * time.Second PullConfigDuration = 30 * time.Second
PushProxyInfoDuration = 30 * time.Second PushProxyInfoDuration = 30 * time.Second
PullClientWorkersDuration = 30 * time.Second
) )
const ( const (
@@ -100,6 +101,50 @@ const (
const ( const (
UserRole_Admin = "admin" UserRole_Admin = "admin"
UserRole_Normal = "normal" UserRole_Normal = "normal"
CapFileName = "workerd.capnp"
WorkerInfoPath = "workers"
WorkerCodePath = "src"
DBTypeSqlite = "sqlite"
DefaultHostName = "127.0.0.1"
DefaultNodeName = "default"
DefaultExternalPath = "/"
DefaultEntry = "entry.js"
DefaultSocketTemplate = "unix-abstract:/tmp/frpp-worker-%s.sock"
DefaultCode = `export default {
async fetch(req, env) {
try {
let resp = new Response("worker: " + req.url + " is online! -- " + new Date())
return resp
} catch(e) {
return new Response(e.stack, { status: 500 })
}
}
};`
DefaultConfigTemplate = `using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "{{.WorkerId}}", worker = .v{{.WorkerId}}Worker),
],
sockets = [
(
name = "{{.WorkerId}}",
address = "{{.Socket.Address}}",
http=(),
service="{{.WorkerId}}"
),
]
);
const v{{.WorkerId}}Worker :Workerd.Worker = (
modules = [
(name = "{{.CodeEntry}}", esModule = embed "src/{{.CodeEntry}}"),
],
compatibilityDate = "2023-04-03",
);`
) )
type TokenStatus string type TokenStatus string
@@ -109,3 +154,23 @@ const (
TokenStatusInactive TokenStatus = "inactive" TokenStatusInactive TokenStatus = "inactive"
TokenStatusRevoked TokenStatus = "revoked" TokenStatusRevoked TokenStatus = "revoked"
) )
const (
KeyNodeName = "node_name"
KeyNodeSecret = "node_secret"
KeyNodeProto = "node_proto"
KeyWorkerProto = "worker_proto"
)
type WorkerStatus string
const (
WorkerStatus_Unknown WorkerStatus = "unknown"
WorkerStatus_Running WorkerStatus = "running"
WorkerStatus_Inactive WorkerStatus = "inactive"
)
const (
FrpProxyAnnotationsKey_Ingress = "ingress"
FrpProxyAnnotationsKey_WorkerId = "worker_id"
)

15
go.mod
View File

@@ -7,7 +7,7 @@ toolchain go1.24.1
require ( require (
github.com/UserExistsError/conpty v0.1.4 github.com/UserExistsError/conpty v0.1.4
github.com/casbin/casbin/v2 v2.105.0 github.com/casbin/casbin/v2 v2.105.0
github.com/casbin/gorm-adapter/v3 v3.32.0 github.com/casbin/gorm-adapter/v3 v3.29.0
github.com/coocood/freecache v1.2.4 github.com/coocood/freecache v1.2.4
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/fatedier/frp v0.62.0 github.com/fatedier/frp v0.62.0
@@ -25,23 +25,26 @@ require (
github.com/jackpal/gateway v1.0.16 github.com/jackpal/gateway v1.0.16
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/kardianos/service v1.2.2 github.com/kardianos/service v1.2.2
github.com/lucasepe/codename v0.2.0
github.com/samber/lo v1.47.0 github.com/samber/lo v1.47.0
github.com/shirou/gopsutil/v4 v4.24.11 github.com/shirou/gopsutil/v4 v4.25.4
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/sourcegraph/conc v0.3.0 github.com/sourcegraph/conc v0.3.0
github.com/spf13/cast v1.7.1 github.com/spf13/cast v1.7.1
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.10.0
github.com/tidwall/pretty v1.2.1 github.com/tidwall/pretty v1.2.1
github.com/tiendc/go-deepcopy v1.2.0 github.com/tiendc/go-deepcopy v1.2.0
go.uber.org/fx v1.23.0 go.uber.org/fx v1.23.0
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.37.0
golang.org/x/net v0.39.0 golang.org/x/net v0.39.0
google.golang.org/grpc v1.67.1 google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.5
gorm.io/driver/mysql v1.5.7 gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9 gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.11
k8s.io/apimachinery v0.28.8 k8s.io/apimachinery v0.28.8
) )
@@ -60,7 +63,7 @@ require (
github.com/coreos/go-oidc/v3 v3.14.1 // indirect github.com/coreos/go-oidc/v3 v3.14.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.1 // indirect github.com/ebitengine/purego v0.8.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
@@ -121,7 +124,6 @@ require (
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
@@ -136,7 +138,6 @@ require (
go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/dig v1.18.1 // indirect go.uber.org/dig v1.18.1 // indirect
go.uber.org/mock v0.5.1 // indirect go.uber.org/mock v0.5.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.26.0 // indirect go.uber.org/zap v1.26.0 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
@@ -154,7 +155,7 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/sqlserver v1.5.3 // indirect gorm.io/driver/sqlserver v1.5.3 // indirect
gorm.io/plugin/dbresolver v1.5.3 // indirect gorm.io/plugin/dbresolver v1.5.2 // indirect
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d // indirect gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
modernc.org/libc v1.22.5 // indirect modernc.org/libc v1.22.5 // indirect

28
go.sum
View File

@@ -38,8 +38,8 @@ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I= github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I=
github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
github.com/casbin/gorm-adapter/v3 v3.32.0 h1:Au+IOILBIE9clox5BJhI2nA3p9t7Ep1ePlupdGbGfus= github.com/casbin/gorm-adapter/v3 v3.29.0 h1:MpbF5JVFYOwVGaTkvwDNUV4k+5CYwAu6v83ofZHRfvM=
github.com/casbin/gorm-adapter/v3 v3.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI= github.com/casbin/gorm-adapter/v3 v3.29.0/go.mod h1:C0Ew2tNYtdvDK1f+yEiKZt8XL0fcaurQhOHgxMSBM54=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -68,8 +68,8 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -228,6 +228,8 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasepe/codename v0.2.0 h1:zkW9mKWSO8jjVIYFyZWE9FPvBtFVJxgMpQcMkf4Vv20=
github.com/lucasepe/codename v0.2.0/go.mod h1:RDcExRuZPWp5Uz+BosvpROFTrxpt5r1vSzBObHdBdDM=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@@ -297,8 +299,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
@@ -372,8 +374,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -475,7 +477,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
@@ -533,6 +534,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
@@ -541,10 +543,10 @@ gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0=
gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU= gorm.io/plugin/dbresolver v1.5.2 h1:Iut7lW4TXNoVs++I+ra3zxjSxTRj4ocIeFEVp4lLhII=
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE= gorm.io/plugin/dbresolver v1.5.2/go.mod h1:jPh59GOQbO7v7v28ZKZPd45tr+u3vyT+8tHdfdfOWcU=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d h1:cCKla0V7sa6eixh74LtGQXakTu5QJEzkcX7DzNRhFOE= gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d h1:cCKla0V7sa6eixh74LtGQXakTu5QJEzkcX7DzNRhFOE=
gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20250425231648-60ec4e7a009d/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -167,3 +167,111 @@ message StartProxyRequest {
message StartProxyResponse { message StartProxyResponse {
optional common.Status status = 1; optional common.Status status = 1;
} }
message CreateWorkerRequest {
optional string client_id = 1;
optional common.Worker worker = 2;
}
message CreateWorkerResponse {
optional common.Status status = 1;
optional string worker_id = 2;
}
message RemoveWorkerRequest {
optional string client_id = 1;
optional string worker_id = 2;
}
message RemoveWorkerResponse {
optional common.Status status = 1;
}
message UpdateWorkerRequest {
repeated string client_ids = 1;
optional common.Worker worker = 2;
}
message UpdateWorkerResponse {
optional common.Status status = 1;
}
message RunWorkerRequest {
optional string client_id = 1;
optional string worker_id = 2;
}
message RunWorkerResponse {
optional common.Status status = 1;
}
message StopWorkerRequest {
optional string client_id = 1;
optional string worker_id = 2;
}
message StopWorkerResponse {
optional common.Status status = 1;
}
message ListWorkersRequest {
optional int32 page = 1;
optional int32 page_size = 2;
optional string keyword = 3;
optional string client_id = 4;
optional string server_id = 5;
}
message ListWorkersResponse {
optional common.Status status = 1;
optional int32 total = 2;
repeated common.Worker workers = 3;
}
// 为 client 在一个 server 创建ingress
message CreateWorkerIngressRequest {
optional string client_id = 1;
optional string server_id = 2;
optional string worker_id = 3;
}
message CreateWorkerIngressResponse {
optional common.Status status = 1;
}
message GetWorkerIngressRequest {
optional string worker_id = 1;
}
message GetWorkerIngressResponse {
optional common.Status status = 1;
repeated common.ProxyConfig proxy_configs = 2;
}
message GetWorkerRequest {
optional string worker_id = 1;
}
message GetWorkerResponse {
optional common.Status status = 1;
optional common.Worker worker = 2;
repeated common.Client clients = 3; // worker 已经部署到的 client
}
message GetWorkerStatusRequest {
optional string worker_id = 1;
}
message GetWorkerStatusResponse {
optional common.Status status = 1;
map<string, string> worker_status = 2; // client_id -> status
}
message InstallWorkerdRequest {
optional string client_id = 1;
optional string download_url = 2;
}
message InstallWorkerdResponse {
optional common.Status status = 1;
}

View File

@@ -97,3 +97,25 @@ message ProxyWorkingStatus {
optional string err = 4; optional string err = 4;
optional string remote_addr = 5; optional string remote_addr = 5;
} }
message Worker {
optional string worker_id = 1;
optional string name = 2; // worker's name, also use at worker routing, must be unique, default is UID
optional uint32 user_id = 3; // worker's user id
optional uint32 tenant_id = 4;
optional Socket socket = 5; // worker's socket, platfrom will obtain free port while init worker
optional string code_entry = 6; // worker's entry file, default is 'entry.js'
optional string code = 7; // worker's code
optional string config_template = 8; // worker's capnp file template
}
// one WorkerList for one workerd instance
message WorkerList {
repeated Worker workers = 1;
optional string nodename = 2; // workerd runner host name, for HA
}
message Socket {
optional string name = 1;
optional string address = 2;
}

View File

@@ -24,6 +24,10 @@ enum Event {
EVENT_STOP_STREAM_LOG = 16; EVENT_STOP_STREAM_LOG = 16;
EVENT_START_PTY_CONNECT = 17; EVENT_START_PTY_CONNECT = 17;
EVENT_GET_PROXY_INFO = 18; EVENT_GET_PROXY_INFO = 18;
EVENT_CREATE_WORKER = 19;
EVENT_REMOVE_WORKER = 20;
EVENT_GET_WORKER_STATUS = 21;
EVENT_INSTALL_WORKERD = 22;
} }
message ServerBase { message ServerBase {
@@ -122,10 +126,20 @@ message PTYServerMessage {
bool done = 4; bool done = 4;
} }
message ListClientWorkersRequest {
ClientBase base = 255;
}
message ListClientWorkersResponse {
common.Status status = 1;
repeated common.Worker workers = 2;
}
service Master { service Master {
rpc ServerSend(stream ClientMessage) returns(stream ServerMessage); rpc ServerSend(stream ClientMessage) returns(stream ServerMessage);
rpc PullClientConfig(PullClientConfigReq) returns(PullClientConfigResp); rpc PullClientConfig(PullClientConfigReq) returns(PullClientConfigResp);
rpc PullServerConfig(PullServerConfigReq) returns(PullServerConfigResp); rpc PullServerConfig(PullServerConfigReq) returns(PullServerConfigResp);
rpc ListClientWorkers(ListClientWorkersRequest) returns(ListClientWorkersResponse);
rpc FRPCAuth(FRPAuthRequest) returns(FRPAuthResponse); rpc FRPCAuth(FRPAuthRequest) returns(FRPAuthResponse);
rpc PushProxyInfo(PushProxyInfoReq) returns(PushProxyInfoResp); rpc PushProxyInfo(PushProxyInfoReq) returns(PushProxyInfoResp);
rpc PushClientStreamLog(stream PushClientStreamLogReq) returns(PushStreamLogResp); rpc PushClientStreamLog(stream PushClientStreamLogReq) returns(PushStreamLogResp);

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils" "github.com/VaalaCat/frp-panel/utils"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/samber/lo" "github.com/samber/lo"
@@ -12,6 +13,7 @@ import (
type Client struct { type Client struct {
*ClientEntity *ClientEntity
Workers []*Worker `json:"workers,omitempty" gorm:"many2many:worker_clients;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
} }
type ClientEntity struct { type ClientEntity struct {
@@ -75,3 +77,22 @@ func (c *ClientEntity) MarshalJSONConfig() ([]byte, error) {
} }
return json.Marshal(cliCfg) return json.Marshal(cliCfg)
} }
func (c *ClientEntity) ToPB() *pb.Client {
resp := &pb.Client{
Id: &c.ClientID,
Secret: &c.ConnectSecret,
Config: lo.ToPtr(string(c.ConfigContent)),
Comment: &c.Comment,
ServerId: &c.ServerID,
Stopped: &c.Stopped,
OriginClientId: &c.OriginClientID,
FrpsUrl: &c.FrpsUrl,
Ephemeral: &c.Ephemeral,
}
if c.LastSeenAt != nil {
resp.LastSeenAt = lo.ToPtr(c.LastSeenAt.UnixMilli())
}
return resp
}

View File

@@ -35,6 +35,9 @@ func (dbm *dbManagerImpl) Init() {
if err := db.AutoMigrate(&HistoryProxyStats{}); err != nil { if err := db.AutoMigrate(&HistoryProxyStats{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&HistoryProxyStats{}).TableName()) logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&HistoryProxyStats{}).TableName())
} }
if err := db.AutoMigrate(&Worker{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&Worker{}).TableName())
}
if err := db.AutoMigrate(&ProxyConfig{}); err != nil { if err := db.AutoMigrate(&ProxyConfig{}); err != nil {
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyConfig{}).TableName()) logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyConfig{}).TableName())
} }

View File

@@ -3,13 +3,19 @@ package models
import ( import (
"fmt" "fmt"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
v1 "github.com/fatedier/frp/pkg/config/v1" v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/samber/lo"
"gorm.io/gorm" "gorm.io/gorm"
) )
type ProxyConfig struct { type ProxyConfig struct {
*gorm.Model *gorm.Model
*ProxyConfigEntity *ProxyConfigEntity
WorkerID string `gorm:"type:varchar(255);index"` // 引用的worker
Worker Worker
} }
type ProxyConfigEntity struct { type ProxyConfigEntity struct {
@@ -53,3 +59,36 @@ func (p *ProxyConfigEntity) GetTypedProxyConfig() (v1.TypedProxyConfig, error) {
err := cfg.UnmarshalJSON(p.Content) err := cfg.UnmarshalJSON(p.Content)
return cfg, err return cfg, err
} }
func (p *ProxyConfig) GetTypedProxyConfig() (v1.TypedProxyConfig, error) {
return p.ProxyConfigEntity.GetTypedProxyConfig()
}
func (p *ProxyConfig) FillClientConfig(cli *ClientEntity) error {
return p.ProxyConfigEntity.FillClientConfig(cli)
}
func (p *ProxyConfig) FillTypedProxyConfig(cfg v1.TypedProxyConfig) error {
annotations := cfg.GetBaseConfig().Annotations
if len(annotations) > 0 {
if annotations[defs.FrpProxyAnnotationsKey_Ingress] != "" && len(annotations[defs.FrpProxyAnnotationsKey_WorkerId]) > 0 {
workerId := annotations[defs.FrpProxyAnnotationsKey_WorkerId]
p.WorkerID = workerId
}
}
return p.ProxyConfigEntity.FillTypedProxyConfig(cfg)
}
func (p *ProxyConfig) ToPB() *pb.ProxyConfig {
return &pb.ProxyConfig{
Id: lo.ToPtr(uint32(p.ID)),
Name: lo.ToPtr(p.Name),
Type: lo.ToPtr(p.Type),
Config: lo.ToPtr(string(p.Content)),
Stopped: lo.ToPtr(p.Stopped),
ServerId: lo.ToPtr(p.ServerID),
ClientId: lo.ToPtr(p.ClientID),
OriginClientId: lo.ToPtr(p.OriginClientID),
}
}

86
models/worker.go Normal file
View File

@@ -0,0 +1,86 @@
package models
import (
"time"
"github.com/VaalaCat/frp-panel/pb"
"github.com/samber/lo"
"gorm.io/gorm"
)
type WorkerModel struct {
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
type Worker struct {
*WorkerModel
*WorkerEntity
Clients []Client `gorm:"many2many:worker_clients;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
}
type WorkerEntity struct {
ID string `gorm:"type:varchar(255);uniqueIndex;not null;primaryKey"`
Name string `gorm:"type:varchar(255);index"`
UserId uint32 `gorm:"index"`
TenantId uint32 `gorm:"index"`
Socket JSON[*pb.Socket]
CodeEntry string
Code string
ConfigTemplate string
}
func (w *Worker) TableName() string {
return "workers"
}
func (w *WorkerEntity) FromPB(worker *pb.Worker) *WorkerEntity {
w.ID = worker.GetWorkerId()
w.Name = worker.GetName()
w.UserId = uint32(worker.GetUserId())
w.TenantId = uint32(worker.GetTenantId())
w.Socket = JSON[*pb.Socket]{Data: worker.GetSocket()}
w.CodeEntry = worker.GetCodeEntry()
w.Code = worker.GetCode()
w.ConfigTemplate = worker.GetConfigTemplate()
return w
}
func (w *WorkerEntity) ToPB() *pb.Worker {
return &pb.Worker{
WorkerId: lo.ToPtr(w.ID),
Name: lo.ToPtr(w.Name),
UserId: lo.ToPtr(uint32(w.UserId)),
TenantId: lo.ToPtr(uint32(w.TenantId)),
Socket: w.Socket.Data,
CodeEntry: lo.ToPtr(w.CodeEntry),
Code: lo.ToPtr(w.Code),
ConfigTemplate: lo.ToPtr(w.ConfigTemplate),
}
}
func (w *Worker) FromPB(worker *pb.Worker) *Worker {
if w.WorkerEntity == nil {
w.WorkerEntity = &WorkerEntity{}
}
if w.WorkerModel == nil {
w.WorkerModel = &WorkerModel{}
}
w.WorkerEntity = w.WorkerEntity.FromPB(worker)
return w
}
func (w *Worker) ToPB() *pb.Worker {
if w.WorkerEntity == nil {
w.WorkerEntity = &WorkerEntity{}
}
if w.WorkerModel == nil {
w.WorkerModel = &WorkerModel{}
}
ret := w.WorkerEntity.ToPB()
return ret
}

File diff suppressed because it is too large Load Diff

View File

@@ -871,6 +871,211 @@ func (x *ProxyWorkingStatus) GetRemoteAddr() string {
return "" return ""
} }
type Worker struct {
state protoimpl.MessageState `protogen:"open.v1"`
WorkerId *string `protobuf:"bytes,1,opt,name=worker_id,json=workerId,proto3,oneof" json:"worker_id,omitempty"`
Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` // worker's name, also use at worker routing, must be unique, default is UID
UserId *uint32 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3,oneof" json:"user_id,omitempty"` // worker's user id
TenantId *uint32 `protobuf:"varint,4,opt,name=tenant_id,json=tenantId,proto3,oneof" json:"tenant_id,omitempty"`
Socket *Socket `protobuf:"bytes,5,opt,name=socket,proto3,oneof" json:"socket,omitempty"` // worker's socket, platfrom will obtain free port while init worker
CodeEntry *string `protobuf:"bytes,6,opt,name=code_entry,json=codeEntry,proto3,oneof" json:"code_entry,omitempty"` // worker's entry file, default is 'entry.js'
Code *string `protobuf:"bytes,7,opt,name=code,proto3,oneof" json:"code,omitempty"` // worker's code
ConfigTemplate *string `protobuf:"bytes,8,opt,name=config_template,json=configTemplate,proto3,oneof" json:"config_template,omitempty"` // worker's capnp file template
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Worker) Reset() {
*x = Worker{}
mi := &file_common_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Worker) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Worker) ProtoMessage() {}
func (x *Worker) ProtoReflect() protoreflect.Message {
mi := &file_common_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Worker.ProtoReflect.Descriptor instead.
func (*Worker) Descriptor() ([]byte, []int) {
return file_common_proto_rawDescGZIP(), []int{9}
}
func (x *Worker) GetWorkerId() string {
if x != nil && x.WorkerId != nil {
return *x.WorkerId
}
return ""
}
func (x *Worker) GetName() string {
if x != nil && x.Name != nil {
return *x.Name
}
return ""
}
func (x *Worker) GetUserId() uint32 {
if x != nil && x.UserId != nil {
return *x.UserId
}
return 0
}
func (x *Worker) GetTenantId() uint32 {
if x != nil && x.TenantId != nil {
return *x.TenantId
}
return 0
}
func (x *Worker) GetSocket() *Socket {
if x != nil {
return x.Socket
}
return nil
}
func (x *Worker) GetCodeEntry() string {
if x != nil && x.CodeEntry != nil {
return *x.CodeEntry
}
return ""
}
func (x *Worker) GetCode() string {
if x != nil && x.Code != nil {
return *x.Code
}
return ""
}
func (x *Worker) GetConfigTemplate() string {
if x != nil && x.ConfigTemplate != nil {
return *x.ConfigTemplate
}
return ""
}
// one WorkerList for one workerd instance
type WorkerList struct {
state protoimpl.MessageState `protogen:"open.v1"`
Workers []*Worker `protobuf:"bytes,1,rep,name=workers,proto3" json:"workers,omitempty"`
Nodename *string `protobuf:"bytes,2,opt,name=nodename,proto3,oneof" json:"nodename,omitempty"` // workerd runner host name, for HA
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *WorkerList) Reset() {
*x = WorkerList{}
mi := &file_common_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *WorkerList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WorkerList) ProtoMessage() {}
func (x *WorkerList) ProtoReflect() protoreflect.Message {
mi := &file_common_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WorkerList.ProtoReflect.Descriptor instead.
func (*WorkerList) Descriptor() ([]byte, []int) {
return file_common_proto_rawDescGZIP(), []int{10}
}
func (x *WorkerList) GetWorkers() []*Worker {
if x != nil {
return x.Workers
}
return nil
}
func (x *WorkerList) GetNodename() string {
if x != nil && x.Nodename != nil {
return *x.Nodename
}
return ""
}
type Socket struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name *string `protobuf:"bytes,1,opt,name=name,proto3,oneof" json:"name,omitempty"`
Address *string `protobuf:"bytes,2,opt,name=address,proto3,oneof" json:"address,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Socket) Reset() {
*x = Socket{}
mi := &file_common_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Socket) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Socket) ProtoMessage() {}
func (x *Socket) ProtoReflect() protoreflect.Message {
mi := &file_common_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Socket.ProtoReflect.Descriptor instead.
func (*Socket) Descriptor() ([]byte, []int) {
return file_common_proto_rawDescGZIP(), []int{11}
}
func (x *Socket) GetName() string {
if x != nil && x.Name != nil {
return *x.Name
}
return ""
}
func (x *Socket) GetAddress() string {
if x != nil && x.Address != nil {
return *x.Address
}
return ""
}
var File_common_proto protoreflect.FileDescriptor var File_common_proto protoreflect.FileDescriptor
const file_common_proto_rawDesc = "" + const file_common_proto_rawDesc = "" +
@@ -999,7 +1204,39 @@ const file_common_proto_rawDesc = "" +
"\x05_typeB\t\n" + "\x05_typeB\t\n" +
"\a_statusB\x06\n" + "\a_statusB\x06\n" +
"\x04_errB\x0e\n" + "\x04_errB\x0e\n" +
"\f_remote_addr*\xbc\x01\n" + "\f_remote_addr\"\x83\x03\n" +
"\x06Worker\x12 \n" +
"\tworker_id\x18\x01 \x01(\tH\x00R\bworkerId\x88\x01\x01\x12\x17\n" +
"\x04name\x18\x02 \x01(\tH\x01R\x04name\x88\x01\x01\x12\x1c\n" +
"\auser_id\x18\x03 \x01(\rH\x02R\x06userId\x88\x01\x01\x12 \n" +
"\ttenant_id\x18\x04 \x01(\rH\x03R\btenantId\x88\x01\x01\x12+\n" +
"\x06socket\x18\x05 \x01(\v2\x0e.common.SocketH\x04R\x06socket\x88\x01\x01\x12\"\n" +
"\n" +
"code_entry\x18\x06 \x01(\tH\x05R\tcodeEntry\x88\x01\x01\x12\x17\n" +
"\x04code\x18\a \x01(\tH\x06R\x04code\x88\x01\x01\x12,\n" +
"\x0fconfig_template\x18\b \x01(\tH\aR\x0econfigTemplate\x88\x01\x01B\f\n" +
"\n" +
"_worker_idB\a\n" +
"\x05_nameB\n" +
"\n" +
"\b_user_idB\f\n" +
"\n" +
"_tenant_idB\t\n" +
"\a_socketB\r\n" +
"\v_code_entryB\a\n" +
"\x05_codeB\x12\n" +
"\x10_config_template\"d\n" +
"\n" +
"WorkerList\x12(\n" +
"\aworkers\x18\x01 \x03(\v2\x0e.common.WorkerR\aworkers\x12\x1f\n" +
"\bnodename\x18\x02 \x01(\tH\x00R\bnodename\x88\x01\x01B\v\n" +
"\t_nodename\"U\n" +
"\x06Socket\x12\x17\n" +
"\x04name\x18\x01 \x01(\tH\x00R\x04name\x88\x01\x01\x12\x1d\n" +
"\aaddress\x18\x02 \x01(\tH\x01R\aaddress\x88\x01\x01B\a\n" +
"\x05_nameB\n" +
"\n" +
"\b_address*\xbc\x01\n" +
"\bRespCode\x12\x19\n" + "\bRespCode\x12\x19\n" +
"\x15RESP_CODE_UNSPECIFIED\x10\x00\x12\x15\n" + "\x15RESP_CODE_UNSPECIFIED\x10\x00\x12\x15\n" +
"\x11RESP_CODE_SUCCESS\x10\x01\x12\x17\n" + "\x11RESP_CODE_SUCCESS\x10\x01\x12\x17\n" +
@@ -1027,7 +1264,7 @@ func file_common_proto_rawDescGZIP() []byte {
} }
var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_common_proto_goTypes = []any{ var file_common_proto_goTypes = []any{
(RespCode)(0), // 0: common.RespCode (RespCode)(0), // 0: common.RespCode
(ClientType)(0), // 1: common.ClientType (ClientType)(0), // 1: common.ClientType
@@ -1040,15 +1277,20 @@ var file_common_proto_goTypes = []any{
(*ProxyInfo)(nil), // 8: common.ProxyInfo (*ProxyInfo)(nil), // 8: common.ProxyInfo
(*ProxyConfig)(nil), // 9: common.ProxyConfig (*ProxyConfig)(nil), // 9: common.ProxyConfig
(*ProxyWorkingStatus)(nil), // 10: common.ProxyWorkingStatus (*ProxyWorkingStatus)(nil), // 10: common.ProxyWorkingStatus
(*Worker)(nil), // 11: common.Worker
(*WorkerList)(nil), // 12: common.WorkerList
(*Socket)(nil), // 13: common.Socket
} }
var file_common_proto_depIdxs = []int32{ var file_common_proto_depIdxs = []int32{
0, // 0: common.Status.code:type_name -> common.RespCode 0, // 0: common.Status.code:type_name -> common.RespCode
2, // 1: common.CommonResponse.status:type_name -> common.Status 2, // 1: common.CommonResponse.status:type_name -> common.Status
2, // [2:2] is the sub-list for method output_type 13, // 2: common.Worker.socket:type_name -> common.Socket
2, // [2:2] is the sub-list for method input_type 11, // 3: common.WorkerList.workers:type_name -> common.Worker
2, // [2:2] is the sub-list for extension type_name 4, // [4:4] is the sub-list for method output_type
2, // [2:2] is the sub-list for extension extendee 4, // [4:4] is the sub-list for method input_type
0, // [0:2] is the sub-list for field type_name 4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
} }
func init() { file_common_proto_init() } func init() { file_common_proto_init() }
@@ -1064,13 +1306,16 @@ func file_common_proto_init() {
file_common_proto_msgTypes[6].OneofWrappers = []any{} file_common_proto_msgTypes[6].OneofWrappers = []any{}
file_common_proto_msgTypes[7].OneofWrappers = []any{} file_common_proto_msgTypes[7].OneofWrappers = []any{}
file_common_proto_msgTypes[8].OneofWrappers = []any{} file_common_proto_msgTypes[8].OneofWrappers = []any{}
file_common_proto_msgTypes[9].OneofWrappers = []any{}
file_common_proto_msgTypes[10].OneofWrappers = []any{}
file_common_proto_msgTypes[11].OneofWrappers = []any{}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)),
NumEnums: 2, NumEnums: 2,
NumMessages: 9, NumMessages: 12,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@@ -43,6 +43,10 @@ const (
Event_EVENT_STOP_STREAM_LOG Event = 16 Event_EVENT_STOP_STREAM_LOG Event = 16
Event_EVENT_START_PTY_CONNECT Event = 17 Event_EVENT_START_PTY_CONNECT Event = 17
Event_EVENT_GET_PROXY_INFO Event = 18 Event_EVENT_GET_PROXY_INFO Event = 18
Event_EVENT_CREATE_WORKER Event = 19
Event_EVENT_REMOVE_WORKER Event = 20
Event_EVENT_GET_WORKER_STATUS Event = 21
Event_EVENT_INSTALL_WORKERD Event = 22
) )
// Enum value maps for Event. // Enum value maps for Event.
@@ -67,6 +71,10 @@ var (
16: "EVENT_STOP_STREAM_LOG", 16: "EVENT_STOP_STREAM_LOG",
17: "EVENT_START_PTY_CONNECT", 17: "EVENT_START_PTY_CONNECT",
18: "EVENT_GET_PROXY_INFO", 18: "EVENT_GET_PROXY_INFO",
19: "EVENT_CREATE_WORKER",
20: "EVENT_REMOVE_WORKER",
21: "EVENT_GET_WORKER_STATUS",
22: "EVENT_INSTALL_WORKERD",
} }
Event_value = map[string]int32{ Event_value = map[string]int32{
"EVENT_UNSPECIFIED": 0, "EVENT_UNSPECIFIED": 0,
@@ -88,6 +96,10 @@ var (
"EVENT_STOP_STREAM_LOG": 16, "EVENT_STOP_STREAM_LOG": 16,
"EVENT_START_PTY_CONNECT": 17, "EVENT_START_PTY_CONNECT": 17,
"EVENT_GET_PROXY_INFO": 18, "EVENT_GET_PROXY_INFO": 18,
"EVENT_CREATE_WORKER": 19,
"EVENT_REMOVE_WORKER": 20,
"EVENT_GET_WORKER_STATUS": 21,
"EVENT_INSTALL_WORKERD": 22,
} }
) )
@@ -1096,6 +1108,102 @@ func (x *PTYServerMessage) GetDone() bool {
return false return false
} }
type ListClientWorkersRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Base *ClientBase `protobuf:"bytes,255,opt,name=base,proto3" json:"base,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListClientWorkersRequest) Reset() {
*x = ListClientWorkersRequest{}
mi := &file_rpc_master_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListClientWorkersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListClientWorkersRequest) ProtoMessage() {}
func (x *ListClientWorkersRequest) ProtoReflect() protoreflect.Message {
mi := &file_rpc_master_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListClientWorkersRequest.ProtoReflect.Descriptor instead.
func (*ListClientWorkersRequest) Descriptor() ([]byte, []int) {
return file_rpc_master_proto_rawDescGZIP(), []int{17}
}
func (x *ListClientWorkersRequest) GetBase() *ClientBase {
if x != nil {
return x.Base
}
return nil
}
type ListClientWorkersResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status *Status `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
Workers []*Worker `protobuf:"bytes,2,rep,name=workers,proto3" json:"workers,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListClientWorkersResponse) Reset() {
*x = ListClientWorkersResponse{}
mi := &file_rpc_master_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListClientWorkersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListClientWorkersResponse) ProtoMessage() {}
func (x *ListClientWorkersResponse) ProtoReflect() protoreflect.Message {
mi := &file_rpc_master_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListClientWorkersResponse.ProtoReflect.Descriptor instead.
func (*ListClientWorkersResponse) Descriptor() ([]byte, []int) {
return file_rpc_master_proto_rawDescGZIP(), []int{18}
}
func (x *ListClientWorkersResponse) GetStatus() *Status {
if x != nil {
return x.Status
}
return nil
}
func (x *ListClientWorkersResponse) GetWorkers() []*Worker {
if x != nil {
return x.Workers
}
return nil
}
var File_rpc_master_proto protoreflect.FileDescriptor var File_rpc_master_proto protoreflect.FileDescriptor
const file_rpc_master_proto_rawDesc = "" + const file_rpc_master_proto_rawDesc = "" +
@@ -1172,7 +1280,12 @@ const file_rpc_master_proto_rawDesc = "" +
"\x04done\x18\x04 \x01(\bR\x04doneB\a\n" + "\x04done\x18\x04 \x01(\bR\x04doneB\a\n" +
"\x05_dataB\t\n" + "\x05_dataB\t\n" +
"\a_heightB\b\n" + "\a_heightB\b\n" +
"\x06_width*\xb5\x03\n" + "\x06_width\"C\n" +
"\x18ListClientWorkersRequest\x12'\n" +
"\x04base\x18\xff\x01 \x01(\v2\x12.master.ClientBaseR\x04base\"m\n" +
"\x19ListClientWorkersResponse\x12&\n" +
"\x06status\x18\x01 \x01(\v2\x0e.common.StatusR\x06status\x12(\n" +
"\aworkers\x18\x02 \x03(\v2\x0e.common.WorkerR\aworkers*\x9f\x04\n" +
"\x05Event\x12\x15\n" + "\x05Event\x12\x15\n" +
"\x11EVENT_UNSPECIFIED\x10\x00\x12\x19\n" + "\x11EVENT_UNSPECIFIED\x10\x00\x12\x19\n" +
"\x15EVENT_REGISTER_CLIENT\x10\x01\x12\x19\n" + "\x15EVENT_REGISTER_CLIENT\x10\x01\x12\x19\n" +
@@ -1196,12 +1309,17 @@ const file_rpc_master_proto_rawDesc = "" +
"\x16EVENT_START_STREAM_LOG\x10\x0f\x12\x19\n" + "\x16EVENT_START_STREAM_LOG\x10\x0f\x12\x19\n" +
"\x15EVENT_STOP_STREAM_LOG\x10\x10\x12\x1b\n" + "\x15EVENT_STOP_STREAM_LOG\x10\x10\x12\x1b\n" +
"\x17EVENT_START_PTY_CONNECT\x10\x11\x12\x18\n" + "\x17EVENT_START_PTY_CONNECT\x10\x11\x12\x18\n" +
"\x14EVENT_GET_PROXY_INFO\x10\x122\xd7\x04\n" + "\x14EVENT_GET_PROXY_INFO\x10\x12\x12\x17\n" +
"\x13EVENT_CREATE_WORKER\x10\x13\x12\x17\n" +
"\x13EVENT_REMOVE_WORKER\x10\x14\x12\x1b\n" +
"\x17EVENT_GET_WORKER_STATUS\x10\x15\x12\x19\n" +
"\x15EVENT_INSTALL_WORKERD\x10\x162\xb1\x05\n" +
"\x06Master\x12>\n" + "\x06Master\x12>\n" +
"\n" + "\n" +
"ServerSend\x12\x15.master.ClientMessage\x1a\x15.master.ServerMessage(\x010\x01\x12M\n" + "ServerSend\x12\x15.master.ClientMessage\x1a\x15.master.ServerMessage(\x010\x01\x12M\n" +
"\x10PullClientConfig\x12\x1b.master.PullClientConfigReq\x1a\x1c.master.PullClientConfigResp\x12M\n" + "\x10PullClientConfig\x12\x1b.master.PullClientConfigReq\x1a\x1c.master.PullClientConfigResp\x12M\n" +
"\x10PullServerConfig\x12\x1b.master.PullServerConfigReq\x1a\x1c.master.PullServerConfigResp\x12;\n" + "\x10PullServerConfig\x12\x1b.master.PullServerConfigReq\x1a\x1c.master.PullServerConfigResp\x12X\n" +
"\x11ListClientWorkers\x12 .master.ListClientWorkersRequest\x1a!.master.ListClientWorkersResponse\x12;\n" +
"\bFRPCAuth\x12\x16.master.FRPAuthRequest\x1a\x17.master.FRPAuthResponse\x12D\n" + "\bFRPCAuth\x12\x16.master.FRPAuthRequest\x1a\x17.master.FRPAuthResponse\x12D\n" +
"\rPushProxyInfo\x12\x18.master.PushProxyInfoReq\x1a\x19.master.PushProxyInfoResp\x12R\n" + "\rPushProxyInfo\x12\x18.master.PushProxyInfoReq\x1a\x19.master.PushProxyInfoResp\x12R\n" +
"\x13PushClientStreamLog\x12\x1e.master.PushClientStreamLogReq\x1a\x19.master.PushStreamLogResp(\x01\x12R\n" + "\x13PushClientStreamLog\x12\x1e.master.PushClientStreamLogReq\x1a\x19.master.PushStreamLogResp(\x01\x12R\n" +
@@ -1222,71 +1340,79 @@ func file_rpc_master_proto_rawDescGZIP() []byte {
} }
var file_rpc_master_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_rpc_master_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_rpc_master_proto_msgTypes = make([]protoimpl.MessageInfo, 17) var file_rpc_master_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
var file_rpc_master_proto_goTypes = []any{ var file_rpc_master_proto_goTypes = []any{
(Event)(0), // 0: master.Event (Event)(0), // 0: master.Event
(*ServerBase)(nil), // 1: master.ServerBase (*ServerBase)(nil), // 1: master.ServerBase
(*ClientBase)(nil), // 2: master.ClientBase (*ClientBase)(nil), // 2: master.ClientBase
(*ServerMessage)(nil), // 3: master.ServerMessage (*ServerMessage)(nil), // 3: master.ServerMessage
(*ClientMessage)(nil), // 4: master.ClientMessage (*ClientMessage)(nil), // 4: master.ClientMessage
(*PullClientConfigReq)(nil), // 5: master.PullClientConfigReq (*PullClientConfigReq)(nil), // 5: master.PullClientConfigReq
(*PullClientConfigResp)(nil), // 6: master.PullClientConfigResp (*PullClientConfigResp)(nil), // 6: master.PullClientConfigResp
(*PullServerConfigReq)(nil), // 7: master.PullServerConfigReq (*PullServerConfigReq)(nil), // 7: master.PullServerConfigReq
(*PullServerConfigResp)(nil), // 8: master.PullServerConfigResp (*PullServerConfigResp)(nil), // 8: master.PullServerConfigResp
(*FRPAuthRequest)(nil), // 9: master.FRPAuthRequest (*FRPAuthRequest)(nil), // 9: master.FRPAuthRequest
(*FRPAuthResponse)(nil), // 10: master.FRPAuthResponse (*FRPAuthResponse)(nil), // 10: master.FRPAuthResponse
(*PushProxyInfoReq)(nil), // 11: master.PushProxyInfoReq (*PushProxyInfoReq)(nil), // 11: master.PushProxyInfoReq
(*PushProxyInfoResp)(nil), // 12: master.PushProxyInfoResp (*PushProxyInfoResp)(nil), // 12: master.PushProxyInfoResp
(*PushServerStreamLogReq)(nil), // 13: master.PushServerStreamLogReq (*PushServerStreamLogReq)(nil), // 13: master.PushServerStreamLogReq
(*PushClientStreamLogReq)(nil), // 14: master.PushClientStreamLogReq (*PushClientStreamLogReq)(nil), // 14: master.PushClientStreamLogReq
(*PushStreamLogResp)(nil), // 15: master.PushStreamLogResp (*PushStreamLogResp)(nil), // 15: master.PushStreamLogResp
(*PTYClientMessage)(nil), // 16: master.PTYClientMessage (*PTYClientMessage)(nil), // 16: master.PTYClientMessage
(*PTYServerMessage)(nil), // 17: master.PTYServerMessage (*PTYServerMessage)(nil), // 17: master.PTYServerMessage
(*Status)(nil), // 18: common.Status (*ListClientWorkersRequest)(nil), // 18: master.ListClientWorkersRequest
(*Client)(nil), // 19: common.Client (*ListClientWorkersResponse)(nil), // 19: master.ListClientWorkersResponse
(*Server)(nil), // 20: common.Server (*Status)(nil), // 20: common.Status
(*ProxyInfo)(nil), // 21: common.ProxyInfo (*Client)(nil), // 21: common.Client
(*Server)(nil), // 22: common.Server
(*ProxyInfo)(nil), // 23: common.ProxyInfo
(*Worker)(nil), // 24: common.Worker
} }
var file_rpc_master_proto_depIdxs = []int32{ var file_rpc_master_proto_depIdxs = []int32{
0, // 0: master.ServerMessage.event:type_name -> master.Event 0, // 0: master.ServerMessage.event:type_name -> master.Event
0, // 1: master.ClientMessage.event:type_name -> master.Event 0, // 1: master.ClientMessage.event:type_name -> master.Event
2, // 2: master.PullClientConfigReq.base:type_name -> master.ClientBase 2, // 2: master.PullClientConfigReq.base:type_name -> master.ClientBase
18, // 3: master.PullClientConfigResp.status:type_name -> common.Status 20, // 3: master.PullClientConfigResp.status:type_name -> common.Status
19, // 4: master.PullClientConfigResp.client:type_name -> common.Client 21, // 4: master.PullClientConfigResp.client:type_name -> common.Client
1, // 5: master.PullServerConfigReq.base:type_name -> master.ServerBase 1, // 5: master.PullServerConfigReq.base:type_name -> master.ServerBase
18, // 6: master.PullServerConfigResp.status:type_name -> common.Status 20, // 6: master.PullServerConfigResp.status:type_name -> common.Status
20, // 7: master.PullServerConfigResp.server:type_name -> common.Server 22, // 7: master.PullServerConfigResp.server:type_name -> common.Server
1, // 8: master.FRPAuthRequest.base:type_name -> master.ServerBase 1, // 8: master.FRPAuthRequest.base:type_name -> master.ServerBase
18, // 9: master.FRPAuthResponse.status:type_name -> common.Status 20, // 9: master.FRPAuthResponse.status:type_name -> common.Status
1, // 10: master.PushProxyInfoReq.base:type_name -> master.ServerBase 1, // 10: master.PushProxyInfoReq.base:type_name -> master.ServerBase
21, // 11: master.PushProxyInfoReq.proxy_infos:type_name -> common.ProxyInfo 23, // 11: master.PushProxyInfoReq.proxy_infos:type_name -> common.ProxyInfo
18, // 12: master.PushProxyInfoResp.status:type_name -> common.Status 20, // 12: master.PushProxyInfoResp.status:type_name -> common.Status
1, // 13: master.PushServerStreamLogReq.base:type_name -> master.ServerBase 1, // 13: master.PushServerStreamLogReq.base:type_name -> master.ServerBase
2, // 14: master.PushClientStreamLogReq.base:type_name -> master.ClientBase 2, // 14: master.PushClientStreamLogReq.base:type_name -> master.ClientBase
18, // 15: master.PushStreamLogResp.status:type_name -> common.Status 20, // 15: master.PushStreamLogResp.status:type_name -> common.Status
1, // 16: master.PTYClientMessage.server_base:type_name -> master.ServerBase 1, // 16: master.PTYClientMessage.server_base:type_name -> master.ServerBase
2, // 17: master.PTYClientMessage.client_base:type_name -> master.ClientBase 2, // 17: master.PTYClientMessage.client_base:type_name -> master.ClientBase
4, // 18: master.Master.ServerSend:input_type -> master.ClientMessage 2, // 18: master.ListClientWorkersRequest.base:type_name -> master.ClientBase
5, // 19: master.Master.PullClientConfig:input_type -> master.PullClientConfigReq 20, // 19: master.ListClientWorkersResponse.status:type_name -> common.Status
7, // 20: master.Master.PullServerConfig:input_type -> master.PullServerConfigReq 24, // 20: master.ListClientWorkersResponse.workers:type_name -> common.Worker
9, // 21: master.Master.FRPCAuth:input_type -> master.FRPAuthRequest 4, // 21: master.Master.ServerSend:input_type -> master.ClientMessage
11, // 22: master.Master.PushProxyInfo:input_type -> master.PushProxyInfoReq 5, // 22: master.Master.PullClientConfig:input_type -> master.PullClientConfigReq
14, // 23: master.Master.PushClientStreamLog:input_type -> master.PushClientStreamLogReq 7, // 23: master.Master.PullServerConfig:input_type -> master.PullServerConfigReq
13, // 24: master.Master.PushServerStreamLog:input_type -> master.PushServerStreamLogReq 18, // 24: master.Master.ListClientWorkers:input_type -> master.ListClientWorkersRequest
16, // 25: master.Master.PTYConnect:input_type -> master.PTYClientMessage 9, // 25: master.Master.FRPCAuth:input_type -> master.FRPAuthRequest
3, // 26: master.Master.ServerSend:output_type -> master.ServerMessage 11, // 26: master.Master.PushProxyInfo:input_type -> master.PushProxyInfoReq
6, // 27: master.Master.PullClientConfig:output_type -> master.PullClientConfigResp 14, // 27: master.Master.PushClientStreamLog:input_type -> master.PushClientStreamLogReq
8, // 28: master.Master.PullServerConfig:output_type -> master.PullServerConfigResp 13, // 28: master.Master.PushServerStreamLog:input_type -> master.PushServerStreamLogReq
10, // 29: master.Master.FRPCAuth:output_type -> master.FRPAuthResponse 16, // 29: master.Master.PTYConnect:input_type -> master.PTYClientMessage
12, // 30: master.Master.PushProxyInfo:output_type -> master.PushProxyInfoResp 3, // 30: master.Master.ServerSend:output_type -> master.ServerMessage
15, // 31: master.Master.PushClientStreamLog:output_type -> master.PushStreamLogResp 6, // 31: master.Master.PullClientConfig:output_type -> master.PullClientConfigResp
15, // 32: master.Master.PushServerStreamLog:output_type -> master.PushStreamLogResp 8, // 32: master.Master.PullServerConfig:output_type -> master.PullServerConfigResp
17, // 33: master.Master.PTYConnect:output_type -> master.PTYServerMessage 19, // 33: master.Master.ListClientWorkers:output_type -> master.ListClientWorkersResponse
26, // [26:34] is the sub-list for method output_type 10, // 34: master.Master.FRPCAuth:output_type -> master.FRPAuthResponse
18, // [18:26] is the sub-list for method input_type 12, // 35: master.Master.PushProxyInfo:output_type -> master.PushProxyInfoResp
18, // [18:18] is the sub-list for extension type_name 15, // 36: master.Master.PushClientStreamLog:output_type -> master.PushStreamLogResp
18, // [18:18] is the sub-list for extension extendee 15, // 37: master.Master.PushServerStreamLog:output_type -> master.PushStreamLogResp
0, // [0:18] is the sub-list for field type_name 17, // 38: master.Master.PTYConnect:output_type -> master.PTYServerMessage
30, // [30:39] is the sub-list for method output_type
21, // [21:30] is the sub-list for method input_type
21, // [21:21] is the sub-list for extension type_name
21, // [21:21] is the sub-list for extension extendee
0, // [0:21] is the sub-list for field type_name
} }
func init() { file_rpc_master_proto_init() } func init() { file_rpc_master_proto_init() }
@@ -1306,7 +1432,7 @@ func file_rpc_master_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_master_proto_rawDesc), len(file_rpc_master_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_master_proto_rawDesc), len(file_rpc_master_proto_rawDesc)),
NumEnums: 1, NumEnums: 1,
NumMessages: 17, NumMessages: 19,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -22,6 +22,7 @@ const (
Master_ServerSend_FullMethodName = "/master.Master/ServerSend" Master_ServerSend_FullMethodName = "/master.Master/ServerSend"
Master_PullClientConfig_FullMethodName = "/master.Master/PullClientConfig" Master_PullClientConfig_FullMethodName = "/master.Master/PullClientConfig"
Master_PullServerConfig_FullMethodName = "/master.Master/PullServerConfig" Master_PullServerConfig_FullMethodName = "/master.Master/PullServerConfig"
Master_ListClientWorkers_FullMethodName = "/master.Master/ListClientWorkers"
Master_FRPCAuth_FullMethodName = "/master.Master/FRPCAuth" Master_FRPCAuth_FullMethodName = "/master.Master/FRPCAuth"
Master_PushProxyInfo_FullMethodName = "/master.Master/PushProxyInfo" Master_PushProxyInfo_FullMethodName = "/master.Master/PushProxyInfo"
Master_PushClientStreamLog_FullMethodName = "/master.Master/PushClientStreamLog" Master_PushClientStreamLog_FullMethodName = "/master.Master/PushClientStreamLog"
@@ -36,6 +37,7 @@ type MasterClient interface {
ServerSend(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ClientMessage, ServerMessage], error) ServerSend(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ClientMessage, ServerMessage], error)
PullClientConfig(ctx context.Context, in *PullClientConfigReq, opts ...grpc.CallOption) (*PullClientConfigResp, error) PullClientConfig(ctx context.Context, in *PullClientConfigReq, opts ...grpc.CallOption) (*PullClientConfigResp, error)
PullServerConfig(ctx context.Context, in *PullServerConfigReq, opts ...grpc.CallOption) (*PullServerConfigResp, error) PullServerConfig(ctx context.Context, in *PullServerConfigReq, opts ...grpc.CallOption) (*PullServerConfigResp, error)
ListClientWorkers(ctx context.Context, in *ListClientWorkersRequest, opts ...grpc.CallOption) (*ListClientWorkersResponse, error)
FRPCAuth(ctx context.Context, in *FRPAuthRequest, opts ...grpc.CallOption) (*FRPAuthResponse, error) FRPCAuth(ctx context.Context, in *FRPAuthRequest, opts ...grpc.CallOption) (*FRPAuthResponse, error)
PushProxyInfo(ctx context.Context, in *PushProxyInfoReq, opts ...grpc.CallOption) (*PushProxyInfoResp, error) PushProxyInfo(ctx context.Context, in *PushProxyInfoReq, opts ...grpc.CallOption) (*PushProxyInfoResp, error)
PushClientStreamLog(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PushClientStreamLogReq, PushStreamLogResp], error) PushClientStreamLog(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PushClientStreamLogReq, PushStreamLogResp], error)
@@ -84,6 +86,16 @@ func (c *masterClient) PullServerConfig(ctx context.Context, in *PullServerConfi
return out, nil return out, nil
} }
func (c *masterClient) ListClientWorkers(ctx context.Context, in *ListClientWorkersRequest, opts ...grpc.CallOption) (*ListClientWorkersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListClientWorkersResponse)
err := c.cc.Invoke(ctx, Master_ListClientWorkers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *masterClient) FRPCAuth(ctx context.Context, in *FRPAuthRequest, opts ...grpc.CallOption) (*FRPAuthResponse, error) { func (c *masterClient) FRPCAuth(ctx context.Context, in *FRPAuthRequest, opts ...grpc.CallOption) (*FRPAuthResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(FRPAuthResponse) out := new(FRPAuthResponse)
@@ -150,6 +162,7 @@ type MasterServer interface {
ServerSend(grpc.BidiStreamingServer[ClientMessage, ServerMessage]) error ServerSend(grpc.BidiStreamingServer[ClientMessage, ServerMessage]) error
PullClientConfig(context.Context, *PullClientConfigReq) (*PullClientConfigResp, error) PullClientConfig(context.Context, *PullClientConfigReq) (*PullClientConfigResp, error)
PullServerConfig(context.Context, *PullServerConfigReq) (*PullServerConfigResp, error) PullServerConfig(context.Context, *PullServerConfigReq) (*PullServerConfigResp, error)
ListClientWorkers(context.Context, *ListClientWorkersRequest) (*ListClientWorkersResponse, error)
FRPCAuth(context.Context, *FRPAuthRequest) (*FRPAuthResponse, error) FRPCAuth(context.Context, *FRPAuthRequest) (*FRPAuthResponse, error)
PushProxyInfo(context.Context, *PushProxyInfoReq) (*PushProxyInfoResp, error) PushProxyInfo(context.Context, *PushProxyInfoReq) (*PushProxyInfoResp, error)
PushClientStreamLog(grpc.ClientStreamingServer[PushClientStreamLogReq, PushStreamLogResp]) error PushClientStreamLog(grpc.ClientStreamingServer[PushClientStreamLogReq, PushStreamLogResp]) error
@@ -174,6 +187,9 @@ func (UnimplementedMasterServer) PullClientConfig(context.Context, *PullClientCo
func (UnimplementedMasterServer) PullServerConfig(context.Context, *PullServerConfigReq) (*PullServerConfigResp, error) { func (UnimplementedMasterServer) PullServerConfig(context.Context, *PullServerConfigReq) (*PullServerConfigResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method PullServerConfig not implemented") return nil, status.Errorf(codes.Unimplemented, "method PullServerConfig not implemented")
} }
func (UnimplementedMasterServer) ListClientWorkers(context.Context, *ListClientWorkersRequest) (*ListClientWorkersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListClientWorkers not implemented")
}
func (UnimplementedMasterServer) FRPCAuth(context.Context, *FRPAuthRequest) (*FRPAuthResponse, error) { func (UnimplementedMasterServer) FRPCAuth(context.Context, *FRPAuthRequest) (*FRPAuthResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method FRPCAuth not implemented") return nil, status.Errorf(codes.Unimplemented, "method FRPCAuth not implemented")
} }
@@ -253,6 +269,24 @@ func _Master_PullServerConfig_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Master_ListClientWorkers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListClientWorkersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MasterServer).ListClientWorkers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Master_ListClientWorkers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MasterServer).ListClientWorkers(ctx, req.(*ListClientWorkersRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Master_FRPCAuth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _Master_FRPCAuth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(FRPAuthRequest) in := new(FRPAuthRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -325,6 +359,10 @@ var Master_ServiceDesc = grpc.ServiceDesc{
MethodName: "PullServerConfig", MethodName: "PullServerConfig",
Handler: _Master_PullServerConfig_Handler, Handler: _Master_PullServerConfig_Handler,
}, },
{
MethodName: "ListClientWorkers",
Handler: _Master_ListClientWorkers_Handler,
},
{ {
MethodName: "FRPCAuth", MethodName: "FRPCAuth",
Handler: _Master_FRPCAuth_Handler, Handler: _Master_FRPCAuth_Handler,

View File

@@ -12,20 +12,42 @@ type application struct {
streamLogHookMgr StreamLogHookMgr streamLogHookMgr StreamLogHookMgr
masterCli MasterClient masterCli MasterClient
shellPTYMgr ShellPTYMgr shellPTYMgr ShellPTYMgr
clientLogManager ClientLogManager clientLogManager ClientLogManager
clientRPCHandler ClientRPCHandler clientRPCHandler ClientRPCHandler
dbManager DBManager dbManager DBManager
clientController ClientController clientController ClientController
clientRecvMap *sync.Map clientRecvMap *sync.Map
clientsManager ClientsManager clientsManager ClientsManager
serverHandler ServerHandler serverHandler ServerHandler
serverController ServerController serverController ServerController
rpcCred credentials.TransportCredentials rpcCred credentials.TransportCredentials
conf conf.Config conf conf.Config
currentRole string currentRole string
permManager PermissionManager permManager PermissionManager
enforcer *casbin.Enforcer enforcer *casbin.Enforcer
workerExecManager WorkerExecManager
workersManager WorkersManager
}
// GetWorkersManager implements Application.
func (a *application) GetWorkersManager() WorkersManager {
return a.workersManager
}
// SetWorkersManager implements Application.
func (a *application) SetWorkersManager(w WorkersManager) {
a.workersManager = w
}
// GetWorkerExecManager implements Application.
func (a *application) GetWorkerExecManager() WorkerExecManager {
return a.workerExecManager
}
// SetWorkerExecManager implements Application.
func (a *application) SetWorkerExecManager(w WorkerExecManager) {
a.workerExecManager = w
} }
// GetEnforcer implements Application. // GetEnforcer implements Application.

View File

@@ -43,6 +43,10 @@ type Application interface {
SetEnforcer(*casbin.Enforcer) SetEnforcer(*casbin.Enforcer)
GetPermManager() PermissionManager GetPermManager() PermissionManager
SetPermManager(PermissionManager) SetPermManager(PermissionManager)
GetWorkerExecManager() WorkerExecManager
SetWorkerExecManager(WorkerExecManager)
GetWorkersManager() WorkersManager
SetWorkersManager(WorkersManager)
} }
type Context struct { type Context struct {

View File

@@ -61,7 +61,7 @@ func WrapperServerMsg[T common.ReqType, U common.RespType](appInstance Applicati
cliMsg, err := common.ProtoResp(resp) cliMsg, err := common.ProtoResp(resp)
if err != nil { if err != nil {
logger.Logger(context.Background()).WithError(err).Errorf("cannot marshal") logger.Logger(context.Background()).WithError(err).Errorf("cannot marshal, may need to add this type to [getEvent] function")
return &pb.ClientMessage{ return &pb.ClientMessage{
Event: pb.Event_EVENT_ERROR, Event: pb.Event_EVENT_ERROR,
Data: []byte(err.Error()), Data: []byte(err.Error()),

View File

@@ -171,3 +171,30 @@ type PermissionManager interface {
RevokeGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) RevokeGroupPermission(groupID string, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
RevokeUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error) RevokeUserPermission(userID int, objType defs.RBACObj, objID string, action defs.RBACAction, tenantID int) (bool, error)
} }
// services/workerd/exec_manager.go
type WorkerExecManager interface {
RunCmd(workerId string, cwd string, argv []string)
ExitCmd(workerId string)
ExitAllCmd()
UpdateBinaryPath(path string)
}
// services/workerd/workerd.go
type WorkerController interface {
RunWorker(c *Context)
StopWorker(c *Context)
// GetWorkerStatus(c *Context) defs.WorkerStatus
GarbageCollect()
Init(c *Context) error
}
// services/workerd/workers_manager.go
type WorkersManager interface {
GetWorker(ctx *Context, id string) (WorkerController, bool)
RunWorker(ctx *Context, id string, worker WorkerController) error
StopWorker(ctx *Context, id string) error
GetWorkerStatus(ctx *Context, id string) (defs.WorkerStatus, error)
// install workerd bin to workerd bin path, if not specified, use default path /usr/local/bin/workerd
InstallWorkerd(ctx *Context, url string, path string) (string, error)
}

View File

@@ -29,7 +29,7 @@ func (q *queryImpl) ValidateClientSecret(clientID, clientSecret string) (*models
return c.ClientEntity, nil return c.ClientEntity, nil
} }
func (q *queryImpl) AdminGetClientByClientID(clientID string) (*models.ClientEntity, error) { func (q *queryImpl) AdminGetClientByClientID(clientID string) (*models.Client, error) {
if clientID == "" { if clientID == "" {
return nil, fmt.Errorf("invalid client id") return nil, fmt.Errorf("invalid client id")
} }
@@ -43,10 +43,10 @@ func (q *queryImpl) AdminGetClientByClientID(clientID string) (*models.ClientEnt
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c.ClientEntity, nil return c, nil
} }
func (q *queryImpl) GetClientByClientID(userInfo models.UserInfo, clientID string) (*models.ClientEntity, error) { func (q *queryImpl) GetClientByClientID(userInfo models.UserInfo, clientID string) (*models.Client, error) {
if clientID == "" { if clientID == "" {
return nil, fmt.Errorf("invalid client id") return nil, fmt.Errorf("invalid client id")
} }
@@ -62,7 +62,22 @@ func (q *queryImpl) GetClientByClientID(userInfo models.UserInfo, clientID strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c.ClientEntity, nil return c, nil
}
func (q *queryImpl) GetClientsByClientIDs(userInfo models.UserInfo, clientIDs []string) ([]*models.Client, error) {
if len(clientIDs) == 0 {
return nil, fmt.Errorf("invalid client ids")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
cs := []*models.Client{}
err := db.Where("client_id IN ?", clientIDs).Find(&cs).Error
if err != nil {
return nil, err
}
return cs, nil
} }
func (q *queryImpl) GetClientByFilter(userInfo models.UserInfo, client *models.ClientEntity, shadow *bool) (*models.ClientEntity, error) { func (q *queryImpl) GetClientByFilter(userInfo models.UserInfo, client *models.ClientEntity, shadow *bool) (*models.ClientEntity, error) {

View File

@@ -548,3 +548,21 @@ func (q *queryImpl) CountProxyConfigsWithFiltersAndKeyword(userInfo models.UserI
} }
return count, nil return count, nil
} }
func (q *queryImpl) GetProxyConfigsByWorkerId(userInfo models.UserInfo, workerID string) ([]*models.ProxyConfig, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
items := []*models.ProxyConfig{}
err := db.
Where(&models.ProxyConfig{ProxyConfigEntity: &models.ProxyConfigEntity{
UserID: userInfo.GetUserID(),
TenantID: userInfo.GetTenantID(),
},
WorkerID: workerID,
}).
Find(&items).Error
if err != nil {
return nil, err
}
return items, nil
}

166
services/dao/worker.go Normal file
View File

@@ -0,0 +1,166 @@
package dao
import (
"fmt"
"github.com/VaalaCat/frp-panel/models"
)
func (q *queryImpl) CreateWorker(userInfo models.UserInfo, worker *models.Worker) error {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
worker.UserId = uint32(userInfo.GetUserID())
worker.TenantId = uint32(userInfo.GetTenantID())
worker.WorkerModel = nil
return db.Create(worker).Error
}
func (q *queryImpl) DeleteWorker(userInfo models.UserInfo, workerID string) error {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
return db.Unscoped().Where(&models.Worker{
WorkerEntity: &models.WorkerEntity{
ID: workerID,
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Delete(&models.Worker{}).Error
}
func (q *queryImpl) UpdateWorker(userInfo models.UserInfo, worker *models.Worker) error {
if worker.WorkerEntity == nil {
return fmt.Errorf("invalid worker entity")
}
if len(worker.WorkerEntity.ID) == 0 {
return fmt.Errorf("invalid worker id")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
if err := db.Unscoped().Model(&models.Worker{
WorkerEntity: &models.WorkerEntity{
ID: worker.ID,
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Association("Clients").Unscoped().Clear(); err != nil {
return err
}
return db.Where(&models.Worker{
WorkerEntity: &models.WorkerEntity{
ID: worker.ID,
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Save(worker).Error
}
func (q *queryImpl) GetWorkerByWorkerID(userInfo models.UserInfo, workerID string) (*models.Worker, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
w := &models.Worker{}
err := db.Where(&models.Worker{
WorkerEntity: &models.WorkerEntity{
ID: workerID,
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Preload("Clients").First(w).Error
if err != nil {
return nil, err
}
return w, nil
}
func (q *queryImpl) ListWorkers(userInfo models.UserInfo, page, pageSize int) ([]*models.Worker, error) {
if page < 1 || pageSize < 1 || pageSize > 100 {
return nil, fmt.Errorf("invalid page or page size")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
offset := (page - 1) * pageSize
var workers []*models.Worker
err := db.Where(&models.Worker{
WorkerEntity: &models.WorkerEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Offset(offset).Limit(pageSize).Preload("Clients").Find(&workers).Error
if err != nil {
return nil, err
}
return workers, nil
}
func (q *queryImpl) AdminListWorkersByClientID(clientID string) ([]*models.Worker, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
client, err := q.AdminGetClientByClientID(clientID)
if err != nil {
return nil, err
}
err = db.Model(&client).Preload("Workers").First(&client).Error
if err != nil {
return nil, err
}
return client.Workers, nil
}
func (q *queryImpl) ListWorkersWithKeyword(userInfo models.UserInfo, page, pageSize int, keyword string) ([]*models.Worker, error) {
if page < 1 || pageSize < 1 || len(keyword) == 0 || pageSize > 100 {
return nil, fmt.Errorf("invalid page or page size or keyword")
}
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
offset := (page - 1) * pageSize
var workers []*models.Worker
err := db.Where("name like ?", "%"+keyword+"%").
Where(&models.Worker{
WorkerEntity: &models.WorkerEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Offset(offset).Limit(pageSize).Preload("Clients").Find(&workers).Error
if err != nil {
return nil, err
}
return workers, nil
}
func (q *queryImpl) CountWorkers(userInfo models.UserInfo) (int64, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var count int64
err := db.Model(&models.Worker{}).Where(&models.Worker{
WorkerEntity: &models.WorkerEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Count(&count).Error
if err != nil {
return 0, err
}
return count, nil
}
func (q *queryImpl) CountWorkersWithKeyword(userInfo models.UserInfo, keyword string) (int64, error) {
db := q.ctx.GetApp().GetDBManager().GetDefaultDB()
var count int64
err := db.Model(&models.Worker{}).Where("name like ?", "%"+keyword+"%").
Where(&models.Worker{
WorkerEntity: &models.WorkerEntity{
UserId: uint32(userInfo.GetUserID()),
TenantId: uint32(userInfo.GetTenantID()),
},
}).Count(&count).Error
if err != nil {
return 0, err
}
return count, nil
}

View File

@@ -10,6 +10,7 @@ import (
masterserver "github.com/VaalaCat/frp-panel/biz/master/server" masterserver "github.com/VaalaCat/frp-panel/biz/master/server"
"github.com/VaalaCat/frp-panel/biz/master/shell" "github.com/VaalaCat/frp-panel/biz/master/shell"
"github.com/VaalaCat/frp-panel/biz/master/streamlog" "github.com/VaalaCat/frp-panel/biz/master/streamlog"
"github.com/VaalaCat/frp-panel/biz/master/worker"
"github.com/VaalaCat/frp-panel/conf" "github.com/VaalaCat/frp-panel/conf"
"github.com/VaalaCat/frp-panel/defs" "github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb" "github.com/VaalaCat/frp-panel/pb"
@@ -26,6 +27,21 @@ type server struct {
appInstance app.Application appInstance app.Application
} }
// ListClientWorkers implements pb.MasterServer.
func (s *server) ListClientWorkers(ctx context.Context, req *pb.ListClientWorkersRequest) (*pb.ListClientWorkersResponse, error) {
logger.Logger(ctx).Infof("list client workers, clientID: [%+v]", req.GetBase().GetClientId())
appCtx := app.NewContext(ctx, s.appInstance)
if _, err := client.ValidateClientRequest(appCtx, req.GetBase()); err != nil {
logger.Logger(ctx).WithError(err).Errorf("cannot validate client request")
return nil, err
}
logger.Logger(appCtx).Infof("validate client success, clientID: [%+v]", req.GetBase().GetClientId())
return worker.ListClientWorkers(appCtx, req)
}
func newRpcServer(appInstance app.Application, creds credentials.TransportCredentials) *grpc.Server { func newRpcServer(appInstance app.Application, creds credentials.TransportCredentials) *grpc.Server {
// s := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) // s := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
// s := grpc.NewServer(grpc.Creds(creds)) // s := grpc.NewServer(grpc.Creds(creds))

48
services/port/manager.go Normal file
View File

@@ -0,0 +1,48 @@
package tunnel
import (
"context"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
)
type PortManager interface {
ClaimWorkerPort(c context.Context, workerID string) int32
GetWorkerPort(c context.Context, workerID string) (int32, bool)
}
type portManager struct {
portMap *utils.SyncMap[string, int32]
}
func (p *portManager) ClaimWorkerPort(c context.Context, workerID string) int32 {
port, err := utils.GetAvailablePort(defs.DefaultHostName)
if err != nil {
logger.Logger(c).WithError(err).Panic("get available port failed")
}
p.portMap.Store(workerID, int32(port))
return int32(port)
}
func (p *portManager) GetWorkerPort(c context.Context, workerID string) (int32, bool) {
return p.portMap.Load(workerID)
}
var (
mgr PortManager
)
func NewPortManager() PortManager {
return &portManager{
portMap: &utils.SyncMap[string, int32]{},
}
}
func GetPortManager() PortManager {
if mgr == nil {
mgr = NewPortManager()
}
return mgr
}

View File

@@ -0,0 +1,117 @@
//go:build !windows
package workerd
import (
"context"
"os"
"os/exec"
"syscall"
"time"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
)
type workerExecManager struct {
//用于外层循坏的退出
signMap *utils.SyncMap[string, bool]
//用于执行cancel函数
chanMap *utils.SyncMap[string, chan struct{}]
// 可执行文件路径
binaryPath string
// 默认参数
defaultArgs []string
}
// var ExecManager *execManager
func NewExecManager(binPath string, defaultArgs []string) app.WorkerExecManager {
if len(defaultArgs) == 0 {
defaultArgs = []string{"--watch", "--verbose"}
}
return &workerExecManager{
signMap: new(utils.SyncMap[string, bool]),
chanMap: new(utils.SyncMap[string, chan struct{}]),
binaryPath: binPath,
defaultArgs: defaultArgs,
}
}
func (m *workerExecManager) RunCmd(uid string, cwd string, argv []string) {
ctx := context.Background()
logger.Logger(context.Background()).Infof("start to run command, command id: [%s], argv: %s", uid, utils.MarshalForJson(argv))
if _, ok := m.chanMap.Load(uid); ok {
logger.Logger(ctx).Infof("command id: [%s] is already running, ignore", uid)
return
}
c := make(chan struct{})
m.chanMap.Store(uid, c)
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context, uid string, argv []string, m *workerExecManager) {
defer func(uid string, m *workerExecManager) {
m.signMap.Delete(uid)
}(uid, m)
logger.Logger(ctx).Infof("command id: [%s] is running!", uid)
for {
args := []string{}
args = append(args, m.defaultArgs...)
args = append(args, argv...)
cmd := exec.CommandContext(ctx, m.binaryPath, args...)
cmd.Dir = cwd
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: false}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
logger.Logger(ctx).WithError(err).Errorf("command id: [%s] run failed, binary path: [%s], args: %s", uid, m.binaryPath, utils.MarshalForJson(args))
}
if exit, ok := m.signMap.Load(uid); ok && exit {
return
}
time.Sleep(3 * time.Second)
}
}(ctx, uid, argv, m)
go func(cancel context.CancelFunc, uid string, m *workerExecManager) {
defer func(uid string, m *workerExecManager) {
m.chanMap.Delete(uid)
}(uid, m)
if channel, ok := m.chanMap.Load(uid); ok {
<-channel
m.signMap.Store(uid, true)
cancel()
return
} else {
logger.Logger(ctx).Errorf("command id: [%s] is not running!", uid)
return
}
}(cancel, uid, m)
}
func (m *workerExecManager) ExitCmd(uid string) {
if channel, ok := m.chanMap.Load(uid); ok {
channel <- struct{}{}
}
}
func (m *workerExecManager) ExitAllCmd() {
for uid := range m.chanMap.ToMap() {
m.ExitCmd(uid)
}
}
func (m *workerExecManager) UpdateBinaryPath(path string) {
m.binaryPath = path
}

View File

@@ -0,0 +1,40 @@
//go:build windows
package workerd
import (
"context"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils/logger"
)
type workerExecManager struct{}
// ExitAllCmd implements app.WorkerExecManager.
func (w *workerExecManager) ExitAllCmd() {
ctx := context.Background()
logger.Logger(ctx).Errorf("windows has not implemented functions")
}
// ExitCmd implements app.WorkerExecManager.
func (w *workerExecManager) ExitCmd(workerId string) {
ctx := context.Background()
logger.Logger(ctx).Errorf("windows has not implemented functions")
}
// RunCmd implements app.WorkerExecManager.
func (w *workerExecManager) RunCmd(workerId string, cwd string, argv []string) {
ctx := context.Background()
logger.Logger(ctx).Errorf("windows has not implemented functions")
}
// UpdateBinaryPath implements app.WorkerExecManager.
func (w *workerExecManager) UpdateBinaryPath(path string) {
ctx := context.Background()
logger.Logger(ctx).Errorf("windows has not implemented functions")
}
func NewExecManager(binPath string, defaultArgs []string) app.WorkerExecManager {
return &workerExecManager{}
}

44
services/workerd/file.go Normal file
View File

@@ -0,0 +1,44 @@
package workerd
import (
"context"
"path/filepath"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils"
)
func WriteWorkerCodeToFile(ctx context.Context, worker *pb.Worker, workerdCWD string) error {
return utils.WriteFile(
CodeFilePath(ctx, worker, workerdCWD),
string(worker.GetCode()))
}
func CodeFilePath(ctx context.Context, worker *pb.Worker, workerdCWD string) string {
return filepath.Join(
WorkerCWDPath(ctx, worker, workerdCWD),
defs.WorkerCodePath,
worker.GetCodeEntry())
}
func WorkerCodeRootPath(ctx context.Context, worker *pb.Worker, workerdCWD string) string {
return filepath.Join(
WorkerCWDPath(ctx, worker, workerdCWD),
defs.WorkerCodePath)
}
func WorkerCWDPath(ctx context.Context, worker *pb.Worker, workerdCWD string) string {
return filepath.Join(
workerdCWD,
defs.WorkerInfoPath,
worker.GetWorkerId(),
)
}
func ConfigFilePath(ctx context.Context, worker *pb.Worker, workerdCWD string) string {
return filepath.Join(
WorkerCWDPath(ctx, worker, workerdCWD),
defs.CapFileName,
)
}

View File

@@ -0,0 +1,49 @@
package workerd
import (
"fmt"
"strings"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils"
"github.com/samber/lo"
)
type Opt func(*pb.Worker)
func FillWorkerValue(worker *pb.Worker, UserID uint, opt ...Opt) {
worker.UserId = lo.ToPtr(uint32(UserID))
if len(worker.GetName()) == 0 {
worker.Name = lo.ToPtr(utils.NewCodeName(2))
}
if len(worker.GetCode()) == 0 {
worker.Code = lo.ToPtr(string(defs.DefaultCode))
}
if len(worker.GetWorkerId()) == 0 {
worker.WorkerId = lo.ToPtr(utils.GenerateUUID())
}
if len(worker.GetCodeEntry()) == 0 {
worker.CodeEntry = lo.ToPtr(string(defs.DefaultEntry))
}
if len(worker.GetConfigTemplate()) == 0 {
worker.ConfigTemplate = lo.ToPtr(string(defs.DefaultConfigTemplate))
}
worker.Socket = &pb.Socket{
Name: lo.ToPtr(worker.GetWorkerId()),
Address: lo.ToPtr(fmt.Sprintf(defs.DefaultSocketTemplate, worker.GetWorkerId())),
}
for _, o := range opt {
o(worker)
}
}
func SafeWorkerID(id string) string {
replacer := strings.NewReplacer("/", "", ".", "", "-", "")
return replacer.Replace(id)
}

View File

@@ -0,0 +1,93 @@
package workerd
import (
"context"
"os"
"strings"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils/logger"
)
var _ app.WorkerController = (*workerdController)(nil)
type workerdController struct {
worker *pb.Worker
workerdCwd string
status *defs.WorkerStatus
}
func NewWorkerdController(worker *pb.Worker, workerdCwd string) *workerdController {
return &workerdController{
worker: worker,
workerdCwd: workerdCwd,
}
}
func (w *workerdController) RunWorker(c *app.Context) {
if err := w.Init(c); err != nil {
logger.Logger(c).WithError(err).Errorf("init worker failed, workerId: [%s]", w.worker.GetWorkerId())
return
}
execMgr := c.GetApp().GetWorkerExecManager()
execMgr.RunCmd(
w.worker.GetWorkerId(), WorkerCWDPath(c, w.worker, w.workerdCwd),
[]string{ConfigFilePath(c, w.worker, w.workerdCwd)},
)
}
func (w *workerdController) StopWorker(c *app.Context) {
execMgr := c.GetApp().GetWorkerExecManager()
execMgr.ExitCmd(w.worker.GetWorkerId())
w.GarbageCollect()
}
func (w *workerdController) GetWorkerStatus(c *app.Context) defs.WorkerStatus {
if w.status == nil {
return defs.WorkerStatus_Unknown
}
return *w.status
}
func (w *workerdController) Init(c *app.Context) error {
workerCodePath := WorkerCodeRootPath(c, w.worker, w.workerdCwd)
// 1. 创建工作目录
if err := os.MkdirAll(workerCodePath, os.ModePerm); err != nil {
logger.Logger(c).WithError(err).Errorf("create work dir failed, path: [%s]", workerCodePath)
return err
}
// 2. 写入配置文件和代码文件
if err := WriteWorkerCodeToFile(c, w.worker, w.workerdCwd); err != nil {
logger.Logger(c).WithError(err).Errorf("write worker code failed, workerId: [%s]", w.worker.GetWorkerId())
return err
}
if err := GenCapnpConfig(c, w.workerdCwd, &pb.WorkerList{Workers: []*pb.Worker{w.worker}}); err != nil {
logger.Logger(c).WithError(err).Errorf("gen worker capnp config failed, workerId: [%s]", w.worker.GetWorkerId())
return err
}
logger.Logger(c).Infof("init worker success, workerId: [%s], code path: [%s]", w.worker.GetWorkerId(), workerCodePath)
return nil
}
func (w *workerdController) GarbageCollect() {
ctx := context.Background()
pathToRemove := WorkerCWDPath(ctx, w.worker, w.workerdCwd)
if !strings.HasPrefix(pathToRemove, "/tmp") {
logger.Logger(ctx).Errorf("path not start with /tmp, do not remove path: [%s]", pathToRemove)
return
}
if err := os.RemoveAll(pathToRemove); err != nil {
logger.Logger(ctx).WithError(err).Errorf("remove path failed, path: [%s]", pathToRemove)
}
}

View File

@@ -0,0 +1,67 @@
package workerd
import (
"bytes"
"errors"
"html/template"
"path/filepath"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils"
"github.com/samber/lo"
)
func BuildCapfile(workers []*pb.Worker) map[string]string {
if len(workers) == 0 {
return map[string]string{}
}
results := map[string]string{}
for _, worker := range workers {
tmpWorker := &pb.Worker{
WorkerId: lo.ToPtr(SafeWorkerID(worker.GetWorkerId())),
UserId: lo.ToPtr(worker.GetUserId()),
CodeEntry: lo.ToPtr(worker.GetCodeEntry()),
Socket: &pb.Socket{
Name: lo.ToPtr(worker.GetWorkerId()),
Address: lo.ToPtr(worker.GetSocket().GetAddress()),
},
ConfigTemplate: lo.ToPtr(worker.GetConfigTemplate()),
}
writer := new(bytes.Buffer)
capTemplate := template.New("capfile")
workerTemplate := tmpWorker.GetConfigTemplate()
if workerTemplate == "" {
workerTemplate = defs.DefaultConfigTemplate
}
capTemplate, err := capTemplate.Parse(workerTemplate)
if err != nil {
panic(err)
}
capTemplate.Execute(writer, tmpWorker)
results[worker.GetWorkerId()] = writer.String()
}
return results
}
func GenWorkerConfig(worker *pb.Worker, dir string) error {
if worker == nil || worker.GetWorkerId() == "" {
return errors.New("error worker")
}
fileMap := BuildCapfile([]*pb.Worker{worker})
fileContent, ok := fileMap[worker.GetWorkerId()]
if !ok {
return errors.New("BuildCapfile error")
}
return utils.WriteFile(
filepath.Join(
dir, defs.WorkerInfoPath,
worker.GetWorkerId(), defs.CapFileName,
), fileContent)
}

View File

@@ -0,0 +1,37 @@
package workerd
import (
"context"
"errors"
"github.com/VaalaCat/frp-panel/pb"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
func GenCapnpConfig(ctx context.Context, workerdDir string, workerList *pb.WorkerList) error {
var hasError bool
for _, worker := range workerList.Workers {
fileMap := BuildCapfile([]*pb.Worker{worker})
if fileContent, ok := fileMap[worker.GetWorkerId()]; ok {
err := utils.WriteFile(
ConfigFilePath(ctx, worker, workerdDir),
fileContent)
if err != nil {
logrus.WithError(err).Errorf("failed to write file, worker is: %+v", worker.Name)
hasError = true
}
}
}
logger.Logger(ctx).Infof("GenCapnpConfig has error: %v, workerList: %+v", hasError,
lo.SliceToMap(workerList.GetWorkers(), func(w *pb.Worker) (string, bool) { return w.GetWorkerId(), true }))
if hasError {
return errors.New("GenCapnpConfig has error")
}
return nil
}

View File

@@ -0,0 +1,93 @@
package workerd
import (
"testing"
"github.com/VaalaCat/frp-panel/pb"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
func TestBuildCapfile(t *testing.T) {
tests := []struct {
name string
wokers []*pb.Worker
expect func(t *testing.T, resp map[string]string)
}{
{
name: "common case",
wokers: []*pb.Worker{
{
WorkerId: lo.ToPtr("test"),
CodeEntry: lo.ToPtr("test/entry.js"),
Socket: &pb.Socket{
Address: lo.ToPtr("unix:/test/test.sock"),
},
},
{
WorkerId: lo.ToPtr("test1"),
CodeEntry: lo.ToPtr("test1/entry.js"),
Socket: &pb.Socket{
Address: lo.ToPtr("unix:/test1/test.sock"),
},
},
},
expect: func(t *testing.T, result map[string]string) {
assert.Equal(t,
`using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "test", worker = .vtestWorker),
],
sockets = [
(
name = "test",
address = "unix:/test/test.sock",
http=(),
service="test"
),
]
);
const vtestWorker :Workerd.Worker = (
modules = [
(name = "test/entry.js", esModule = embed "src/test/entry.js"),
],
compatibilityDate = "2023-04-03",
);`, result["test"])
assert.Equal(t, `using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "test1", worker = .vtest1Worker),
],
sockets = [
(
name = "test1",
address = "unix:/test1/test.sock",
http=(),
service="test1"
),
]
);
const vtest1Worker :Workerd.Worker = (
modules = [
(name = "test1/entry.js", esModule = embed "src/test1/entry.js"),
],
compatibilityDate = "2023-04-03",
);`, result["test1"])
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.expect(t, BuildCapfile(tt.wokers))
})
}
}

View File

@@ -0,0 +1,43 @@
package workerd
import (
"context"
"testing"
"time"
"github.com/VaalaCat/frp-panel/pb"
"github.com/sourcegraph/conc"
)
func TestRunWorker(t *testing.T) {
workerdCWD := "/home/coder/code/frp-panel/tmp/workerd"
workerID := "test"
workerdBinPath := "/home/coder/go/bin/workerd"
c := context.Background()
defaultWorker := &pb.Worker{WorkerId: &workerID}
FillWorkerValue(defaultWorker, 1)
if err := GenCapnpConfig(c, workerdCWD, &pb.WorkerList{Workers: []*pb.Worker{defaultWorker}}); err != nil {
panic(err)
}
var wg conc.WaitGroup
wg.Go(func() {
time.Sleep(10 * time.Second)
})
if err := WriteWorkerCodeToFile(c, defaultWorker, workerdCWD); err != nil {
panic(err)
}
runner := NewExecManager(workerdBinPath,
[]string{"serve", "--watch", "--verbose"})
runner.RunCmd(workerID, WorkerCWDPath(c, defaultWorker, workerdCWD),
[]string{ConfigFilePath(c, defaultWorker, workerdCWD)})
defer runner.ExitAllCmd()
wg.Wait()
}

View File

@@ -0,0 +1,126 @@
package workerd
import (
"fmt"
"runtime"
"github.com/VaalaCat/frp-panel/defs"
"github.com/VaalaCat/frp-panel/services/app"
"github.com/VaalaCat/frp-panel/utils"
"github.com/VaalaCat/frp-panel/utils/logger"
)
type workersManager struct {
workers *utils.SyncMap[string, app.WorkerController]
}
func NewWorkersManager() *workersManager {
return &workersManager{
workers: &utils.SyncMap[string, app.WorkerController]{},
}
}
func (m *workersManager) GetWorker(ctx *app.Context, id string) (app.WorkerController, bool) {
return m.workers.Load(id)
}
func (m *workersManager) RunWorker(ctx *app.Context, id string, worker app.WorkerController) error {
if !ctx.GetApp().GetConfig().Client.Features.EnableFunctions {
logger.Logger(ctx).Errorf("function features are not enabled")
return fmt.Errorf("function features are not enabled")
}
worker.RunWorker(ctx)
m.workers.Store(id, worker)
return nil
}
func (m *workersManager) StopWorker(ctx *app.Context, id string) error {
worker, ok := m.workers.Load(id)
if !ok {
return fmt.Errorf("cannot find worker, id: %s", id)
}
worker.StopWorker(ctx)
m.workers.Delete(id)
return nil
}
func (m *workersManager) StopAll() {
m.workers.Range(func(k string, v app.WorkerController) bool {
v.StopWorker(nil)
return true
})
tmpM := m.workers.ToMap()
for k := range tmpM {
m.workers.Delete(k)
}
}
func (m *workersManager) GetWorkerStatus(ctx *app.Context, id string) (defs.WorkerStatus, error) {
ok, err := utils.ProcessExistsBySelf(id)
if err != nil {
return defs.WorkerStatus_Unknown, err
}
if ok {
return defs.WorkerStatus_Running, nil
}
return defs.WorkerStatus_Inactive, nil
}
func (m *workersManager) InstallWorkerd(ctx *app.Context, url string, installDir string) (string, error) {
arch := runtime.GOARCH
os := runtime.GOOS
workerDownloadCfg := ctx.GetApp().GetConfig().Client.Worker.WorkerdDownloadURL
if os != "linux" {
return "", fmt.Errorf("unsupported os: %s", os)
}
if arch != "amd64" && arch != "arm64" {
return "", fmt.Errorf("unsupported arch: %s", arch)
}
downloadUrl := ""
if len(url) > 0 {
downloadUrl = url
} else {
switch arch {
case "amd64":
downloadUrl = workerDownloadCfg.LinuxX8664
case "arm64":
downloadUrl = workerDownloadCfg.LinuxArm64
default:
return "", fmt.Errorf("unsupported arch: %s", arch)
}
}
if workerDownloadCfg.UseProxy {
if len(ctx.GetApp().GetConfig().App.GithubProxyUrl) > 0 {
downloadUrl = fmt.Sprintf("%s/%s", ctx.GetApp().GetConfig().App.GithubProxyUrl, downloadUrl)
}
}
proxyUrl := ctx.GetApp().GetConfig().HTTP_PROXY
path, err := utils.DownloadFile(ctx, downloadUrl, proxyUrl)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("failed to download workerd, url: %s", downloadUrl)
return "", err
}
if len(installDir) == 0 {
installDir = "/usr/local/bin"
}
finalPath, err := utils.ExtractGZTo(path, "workerd", installDir)
if err != nil {
logger.Logger(ctx).WithError(err).Errorf("failed to extract workerd, path: %s", path)
return "", err
}
logger.Logger(ctx).Infof("workerd installed successfully, path: %s", finalPath)
return finalPath, nil
}

24
utils/addr.go Normal file
View File

@@ -0,0 +1,24 @@
package utils
import (
"fmt"
"strings"
)
func NodeHostPrefix(nodeName, nodeID string) string {
return fmt.Sprintf("%s%s", nodeName, nodeID)
}
func NodeHost(nodeName, nodeID string, domainSuffix string) string {
suffix := strings.Trim(domainSuffix, ".")
return fmt.Sprintf("%s.%s", NodeHostPrefix(nodeName, nodeID), suffix)
}
func WorkerHostPrefix(workerName string) string {
return workerName
}
func WorkerHost(workerName, domainSuffix string) string {
suffix := strings.Trim(domainSuffix, ".")
return fmt.Sprintf("%s.%s", WorkerHostPrefix(workerName), suffix)
}

8
utils/codename.go Normal file
View File

@@ -0,0 +1,8 @@
package utils
import "github.com/lucasepe/codename"
func NewCodeName(tokenLength int) string {
rng, _ := codename.DefaultRNG()
return codename.Generate(rng, tokenLength)
}

134
utils/file.go Normal file
View File

@@ -0,0 +1,134 @@
package utils
import (
"archive/tar"
"archive/zip"
"bytes"
"errors"
"io"
"log"
"os"
"path/filepath"
"strings"
)
const (
httpFileMaxBytes = 100 * (1 << 20) // 50 MB
)
func WriteFile(path string, content string) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
f, err := os.Create(path)
if err != nil {
return err
}
_, err = f.WriteString(content)
if err != nil {
return err
}
return nil
}
func CreateTarFromZip(zipReader *zip.Reader) ([]byte, error) {
var tarBuffer bytes.Buffer
err := writeTarArchive(&tarBuffer, zipReader)
if err != nil {
return nil, err
}
return tarBuffer.Bytes(), nil
}
func writeTarArchive(w io.Writer, zipReader *zip.Reader) error {
tarWriter := tar.NewWriter(w)
defer tarWriter.Close()
for _, file := range zipReader.File {
err := processFileInZipArchive(file, tarWriter)
if err != nil {
return err
}
}
return nil
}
func processFileInZipArchive(file *zip.File, tarWriter *tar.Writer) error {
fileReader, err := file.Open()
if err != nil {
return err
}
defer fileReader.Close()
err = tarWriter.WriteHeader(&tar.Header{
Name: file.Name,
Size: file.FileInfo().Size(),
Mode: int64(file.Mode()),
ModTime: file.Modified,
// Note: Zip archives do not store ownership information.
Uid: 1000,
Gid: 1000,
})
if err != nil {
return err
}
n, err := io.CopyN(tarWriter, fileReader, httpFileMaxBytes)
log.Println(file.Name, n, err)
if errors.Is(err, io.EOF) {
err = nil
}
return err
}
func CreateZipFromTar(tarReader *tar.Reader) ([]byte, error) {
var zipBuffer bytes.Buffer
err := WriteZipArchive(&zipBuffer, tarReader)
if err != nil {
return nil, err
}
return zipBuffer.Bytes(), nil
}
func WriteZipArchive(w io.Writer, tarReader *tar.Reader) error {
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
for {
tarHeader, err := tarReader.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
zipHeader, err := zip.FileInfoHeader(tarHeader.FileInfo())
if err != nil {
return err
}
zipHeader.Name = tarHeader.Name
// Some versions of unzip do not check the mode on a file entry and
// simply assume that entries with a trailing path separator (/) are
// directories, and that everything else is a file. Give them a hint.
if tarHeader.FileInfo().IsDir() && !strings.HasSuffix(tarHeader.Name, "/") {
zipHeader.Name += "/"
}
zipEntry, err := zipWriter.CreateHeader(zipHeader)
if err != nil {
return err
}
_, err = io.CopyN(zipEntry, tarReader, httpFileMaxBytes)
if errors.Is(err, io.EOF) {
err = nil
}
if err != nil {
return err
}
}
return nil // don't need to flush as we call `writer.Close()`
}

View File

@@ -1,8 +1,18 @@
package utils package utils
import ( import (
"compress/gzip"
"context"
"fmt"
"io"
"math/rand"
"os" "os"
"path"
"path/filepath" "path/filepath"
"github.com/VaalaCat/frp-panel/utils/logger"
"github.com/imroc/req/v3"
"go.uber.org/multierr"
) )
func EnsureDirectoryExists(filePath string) error { func EnsureDirectoryExists(filePath string) error {
@@ -16,3 +26,171 @@ func EnsureDirectoryExists(filePath string) error {
} }
return nil return nil
} }
func FindExecutableNames(filter func(name string) bool, extraPaths ...string) ([]string, error) {
pathEnv := os.Getenv("PATH")
if pathEnv == "" {
return nil, fmt.Errorf("PATH environment variable is empty")
}
var results []string
seen := make(map[string]struct{})
var errs error
pathToCheck := extraPaths
pathToCheck = append(pathToCheck, filepath.SplitList(pathEnv)...)
for _, dir := range pathToCheck {
entries, err := os.ReadDir(dir)
if err != nil {
// cannot read this directory at all: skip it silently
continue
}
for _, entry := range entries {
name := entry.Name()
if _, dup := seen[name]; dup {
continue
}
if !filter(name) {
continue
}
// We've got a candidate name; try to stat it
info, err := entry.Info()
if err != nil {
// record the error for this matching name
errs = multierr.Append(errs, err)
continue
}
// skip directories or nonexecutable
if info.IsDir() || info.Mode()&0111 == 0 {
continue
}
results = append(results, path.Join(dir, name))
seen[name] = struct{}{}
}
}
if len(results) > 0 {
return results, nil
}
if errs != nil {
// return only the aggregated errors
return nil, errs
}
// no matches and no filespecific errors
return nil, nil
}
var TmpFileDir = path.Join(os.TempDir(), "vaala-frp-panel-download")
// DownloadFile 下载文件到一个临时文件,返回临时文件路径
func DownloadFile(ctx context.Context, url string, proxyUrl string) (string, error) {
os.MkdirAll(TmpFileDir, 0777)
tmpPath, err := os.MkdirTemp(TmpFileDir, "downloads")
if err != nil {
return "", err
}
tmpFileName := generateRandomFileName("download", ".tmp")
fileFullPath := path.Join(tmpPath, tmpFileName)
cli := req.C()
if len(proxyUrl) > 0 {
cli = cli.SetProxyURL(proxyUrl)
}
err = cli.NewParallelDownload(url).
SetConcurrency(5).
SetSegmentSize(1024 * 1024 * 1).
SetOutputFile(fileFullPath).
SetFileMode(0777).
SetTempRootDir(path.Join(TmpFileDir, "downloads_cache")).
Do()
if err != nil {
logger.Logger(ctx).WithError(err).Error("download file from url error")
return "", err
}
return fileFullPath, nil
}
// generateRandomFileName 生成一个随机文件名
func generateRandomFileName(prefix, extension string) string {
randomStr := randomString(8)
fileName := fmt.Sprintf("%s_%s%s", prefix, randomStr, extension)
return fileName
}
// randomString 生成一个指定长度的随机字符串
func randomString(length int) string {
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
bytes := make([]byte, length)
for i := range bytes {
bytes[i] = charset[rand.Intn(len(charset))]
}
return string(bytes)
}
// ExtractGZTo decompresses the srcGZ file into a temporary directory,
// renames the extracted file to newName, moves it to destDir, and sets executable permissions (0755).
// It returns the full path of the final file on success.
func ExtractGZTo(srcGZ, newName, destDir string) (string, error) {
// 1. Open source .gz file
f, err := os.Open(srcGZ)
if err != nil {
return "", fmt.Errorf("failed to open source gzip file %q: %w", srcGZ, err)
}
defer f.Close()
// 2. Create gzip reader
zr, err := gzip.NewReader(f)
if err != nil {
return "", fmt.Errorf("failed to create gzip reader for %q: %w", srcGZ, err)
}
defer zr.Close()
// 3. Create temporary directory
tmpDir, err := os.MkdirTemp("", "vaala-frp-panel-gz_extract_*")
if err != nil {
return "", fmt.Errorf("failed to create temporary directory: %w", err)
}
// Note: tmpDir is not auto-deleted. Caller may clean up if desired.
// 4. Create the output file in the temp directory with the new name
tmpFilePath := filepath.Join(tmpDir, newName)
outFile, err := os.Create(tmpFilePath)
if err != nil {
return "", fmt.Errorf("failed to create temp file %q: %w", tmpFilePath, err)
}
defer outFile.Close()
// 5. Decompress into temp file
if _, err := io.Copy(outFile, zr); err != nil {
return "", fmt.Errorf("failed to write decompressed data to %q: %w", tmpFilePath, err)
}
// 6. Ensure destination directory exists
if err := os.MkdirAll(destDir, 0755); err != nil {
return "", fmt.Errorf("failed to create destination directory %q: %w", destDir, err)
}
// 7. Move the file to the destination directory
finalPath := filepath.Join(destDir, newName)
if err := os.Rename(tmpFilePath, finalPath); err != nil {
return "", fmt.Errorf("failed to move file to %q: %w", finalPath, err)
}
// 8. Set executable permission
if err := os.Chmod(finalPath, 0755); err != nil {
return "", fmt.Errorf("failed to set executable permission on %q: %w", finalPath, err)
}
return finalPath, nil
}

52
utils/port.go Normal file
View File

@@ -0,0 +1,52 @@
package utils
import (
"fmt"
"net"
"time"
"github.com/sirupsen/logrus"
)
func GetAvailablePort(addr string) (int, error) {
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", addr))
if err != nil {
return 0, err
}
listener, err := net.ListenTCP("tcp", address)
if err != nil {
return 0, err
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port, nil
}
func IsPortAvailable(port int, addr string) bool {
address := fmt.Sprintf("%s:%d", addr, port)
listener, err := net.Listen("tcp", address)
if err != nil {
logrus.Infof("port %s is taken: %s", address, err)
return false
}
defer listener.Close()
return true
}
func WaitForPort(host string, port int) {
for {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err == nil {
conn.Close()
break
}
logrus.Warnf("Target port %s:%d is not open yet, waiting...\n", host, port)
time.Sleep(time.Second * 5)
}
logrus.Infof("Target port %s:%d is open", host, port)
time.Sleep(time.Second * 1)
}

32
utils/process.go Normal file
View File

@@ -0,0 +1,32 @@
package utils
import (
"os"
"strings"
"github.com/shirou/gopsutil/v4/process"
)
func ProcessExistsBySelf(target string) (bool, error) {
selfPID := int32(os.Getpid())
procs, err := process.Processes()
if err != nil {
return false, err
}
for _, p := range procs {
ppid, err := p.Ppid()
if err != nil || ppid != selfPID {
continue
}
cmdline, err := p.Cmdline()
if err != nil {
continue
}
if strings.Contains(cmdline, target) {
return true, nil
}
}
return false, nil
}

View File

@@ -180,3 +180,16 @@ func (s *SyncMap[K, V]) Values() []V {
return values return values
} }
func (s *SyncMap[K, V]) ToMap() map[K]V {
s.mu.Lock()
defer s.mu.Unlock()
var m = make(map[K]V, len(s.m))
for k, v := range s.m {
m[k] = v
}
return m
}

15
utils/uuid.go Normal file
View File

@@ -0,0 +1,15 @@
package utils
import (
"strings"
"github.com/google/uuid"
)
func GenerateUUIDWithoutSeperator() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}
func GenerateUUID() string {
return uuid.New().String()
}

69
www/api/worker.ts Normal file
View File

@@ -0,0 +1,69 @@
import http from '@/api/http'
import { API_PATH } from '@/lib/consts'
import {
CreateWorkerIngressRequest,
CreateWorkerIngressResponse,
CreateWorkerRequest,
CreateWorkerResponse,
GetWorkerIngressRequest,
GetWorkerIngressResponse,
GetWorkerRequest,
GetWorkerResponse,
GetWorkerStatusRequest,
GetWorkerStatusResponse,
InstallWorkerdRequest,
InstallWorkerdResponse,
ListWorkersRequest,
ListWorkersResponse,
RemoveWorkerRequest,
RemoveWorkerResponse,
UpdateWorkerRequest,
UpdateWorkerResponse,
} from '@/lib/pb/api_client'
import { BaseResponse } from '@/types/api'
import { constants } from 'node:buffer'
export const getWorker = async (req: GetWorkerRequest) => {
const res = await http.post(API_PATH + '/worker/get', GetWorkerRequest.toJson(req))
return GetWorkerResponse.fromJson((res.data as BaseResponse).body)
}
export const createWorker = async (req: CreateWorkerRequest) => {
const res = await http.post(API_PATH + '/worker/create', CreateWorkerRequest.toJson(req))
return CreateWorkerResponse.fromJson((res.data as BaseResponse).body)
}
export const updateWorker = async (req: UpdateWorkerRequest) => {
const res = await http.post(API_PATH + '/worker/update', UpdateWorkerRequest.toJson(req))
return UpdateWorkerResponse.fromJson((res.data as BaseResponse).body)
}
export const removeWorker = async (req: RemoveWorkerRequest) => {
const res = await http.post(API_PATH + '/worker/remove', RemoveWorkerRequest.toJson(req))
return RemoveWorkerResponse.fromJson((res.data as BaseResponse).body)
}
export const listWorkers = async (req: ListWorkersRequest) => {
const res = await http.post(API_PATH + '/worker/list', ListWorkersRequest.toJson(req))
return ListWorkersResponse.fromJson((res.data as BaseResponse).body)
}
export const createWorkerIngress = async (req: CreateWorkerIngressRequest) => {
const res = await http.post(API_PATH + '/worker/create_ingress', CreateWorkerIngressRequest.toJson(req))
return CreateWorkerIngressResponse.fromJson((res.data as BaseResponse).body)
}
export const getWorkerIngress = async (req: GetWorkerIngressRequest) => {
const res = await http.post(API_PATH + '/worker/get_ingress', GetWorkerIngressRequest.toJson(req))
return GetWorkerIngressResponse.fromJson((res.data as BaseResponse).body)
}
export const getWorkerStatus = async (req: GetWorkerStatusRequest) => {
const res = await http.post(API_PATH + '/worker/status', GetWorkerStatusRequest.toJson(req))
return GetWorkerStatusResponse.fromJson((res.data as BaseResponse).body)
}
export const installWorkerd = async (req: InstallWorkerdRequest) => {
const res = await http.post(API_PATH + '/client/install_workerd', InstallWorkerdRequest.toJson(req))
return InstallWorkerdResponse.fromJson((res.data as BaseResponse).body)
}

View File

@@ -1,24 +1,24 @@
"use client" 'use client'
import React from 'react' import React from 'react'
import { keepPreviousData, useQuery } from '@tanstack/react-query' import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { listClient } from '@/api/client' import { listClient } from '@/api/client'
import { Combobox } from './combobox' import { Combobox } from './combobox'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Client } from '@/lib/pb/common'
export interface ClientSelectorProps { export interface ClientSelectorProps {
clientID?: string clientID?: string
setClientID: (clientID: string) => void setClientID: (clientID: string) => void
clients?: Client[]
onOpenChange?: () => void onOpenChange?: () => void
} }
export const ClientSelector: React.FC<ClientSelectorProps> = ({ export const ClientSelector: React.FC<ClientSelectorProps> = ({ clientID, setClientID, clients, onOpenChange }) => {
clientID,
setClientID,
onOpenChange
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const handleClientChange = (value: string) => { setClientID(value) } const handleClientChange = (value: string) => {
setClientID(value)
}
const [keyword, setKeyword] = React.useState('') const [keyword, setKeyword] = React.useState('')
const { data: clientList, refetch: refetchClients } = useQuery({ const { data: clientList, refetch: refetchClients } = useQuery({
@@ -27,15 +27,23 @@ export const ClientSelector: React.FC<ClientSelectorProps> = ({
return listClient({ page: 1, pageSize: 8, keyword: keyword }) return listClient({ page: 1, pageSize: 8, keyword: keyword })
}, },
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
enabled: clients === undefined,
}) })
return ( return (
<Combobox <Combobox
placeholder={t('selector.client.placeholder')} placeholder={t('selector.client.placeholder')}
dataList={clientList?.clients.map((client) => ({ dataList={
value: client.id || '', clients !== undefined
label: client.id || '' ? clients.map((client) => ({
})) || []} value: client.id || '',
label: client.id || '',
}))
: clientList?.clients.map((client) => ({
value: client.id || '',
label: client.id || '',
})) || []
}
setValue={handleClientChange} setValue={handleClientChange}
value={clientID} value={clientID}
onKeyWordChange={setKeyword} onKeyWordChange={setKeyword}

View File

@@ -1,11 +1,11 @@
"use client" 'use client'
import { Popover, PopoverTrigger } from "@radix-ui/react-popover" import { Popover, PopoverTrigger } from '@radix-ui/react-popover'
import { Badge } from "../ui/badge" import { Badge } from '../ui/badge'
import { ClientStatus } from "@/lib/pb/api_master" import { ClientStatus } from '@/lib/pb/api_master'
import { PopoverContent } from "../ui/popover" import { PopoverContent } from '../ui/popover'
import { useTranslation } from "react-i18next" import { useTranslation } from 'react-i18next'
import { motion } from "framer-motion" import { motion } from 'framer-motion'
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
import { zhCN, enUS } from 'date-fns/locale' import { zhCN, enUS } from 'date-fns/locale'
@@ -13,31 +13,26 @@ export const ClientDetail = ({ clientStatus }: { clientStatus: ClientStatus }) =
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const locale = i18n.language === 'zh' ? zhCN : enUS const locale = i18n.language === 'zh' ? zhCN : enUS
const connectTime = clientStatus.connectTime ? const connectTime = clientStatus.connectTime
formatDistanceToNow(new Date(parseInt(clientStatus.connectTime.toString())), { ? formatDistanceToNow(new Date(parseInt(clientStatus.connectTime.toString())), {
addSuffix: true, addSuffix: true,
locale locale,
}) : '-' })
: '-'
return ( return (
<Popover> <Popover>
<PopoverTrigger className='flex items-center'> <PopoverTrigger className="flex items-center">
<Badge <Badge
variant="secondary" variant="secondary"
className='text-nowrap rounded-full h-6 hover:bg-secondary/80 transition-colors text-sm' className="text-nowrap rounded-full h-6 hover:bg-secondary/80 transition-colors text-xs"
> >
{clientStatus.version?.gitVersion || 'Unknown'} {clientStatus.version?.gitVersion || 'Unknown'}
</Badge> </Badge>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-72 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-border"> <PopoverContent className="w-72 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-border">
<motion.div <motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2 }}>
initial={{ opacity: 0, y: -10 }} <h3 className="text-base font-semibold mb-3 text-center text-foreground">{t('client.detail.title')}</h3>
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
<h3 className="text-base font-semibold mb-3 text-center text-foreground">
{t('client.detail.title')}
</h3>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center py-1 border-b border-border"> <div className="flex justify-between items-center py-1 border-b border-border">
<span className="text-sm font-medium text-muted-foreground">{t('client.detail.version')}</span> <span className="text-sm font-medium text-muted-foreground">{t('client.detail.version')}</span>
@@ -68,4 +63,4 @@ export const ClientDetail = ({ clientStatus }: { clientStatus: ClientStatus }) =
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) )
} }

View File

@@ -0,0 +1,26 @@
'use client'
import React, { useLayoutEffect } from 'react'
import Editor, { loader } from '@monaco-editor/react'
loader.config({
paths: {
vs: 'https://fastly.jsdelivr.net/npm/monaco-editor@0.36.1/min/vs',
},
})
export interface WorkerEditorProps {
code: string
onChange: (value: string) => void
}
export function WorkerEditor({ code, onChange }: WorkerEditorProps) {
// 强制客户端渲染
useLayoutEffect(() => {}, [])
return (
<div className="h-full">
<Editor height="100%" defaultLanguage="javascript" value={code} onChange={(v) => onChange(v ?? '')} />
</div>
)
}

View File

@@ -146,7 +146,7 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin) const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => { const onSubmit = async (values: z.infer<typeof TCPConfigSchema>) => {
const cfgToSubmit = { ...values, plugin: pluginConfig, type: 'tcp', name: proxyName } as TCPProxyConfig const cfgToSubmit = { ...defaultConfig, ...values, plugin: pluginConfig, type: 'tcp', name: proxyName } as TCPProxyConfig
if (!TypedProxyConfigValid(cfgToSubmit)) { if (!TypedProxyConfigValid(cfgToSubmit)) {
toast.error('Invalid configuration') toast.error('Invalid configuration')
return return
@@ -252,7 +252,7 @@ export const STCPProxyForm: React.FC<ProxyFormProps> = ({
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin) const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
const onSubmit = async (values: z.infer<typeof STCPConfigSchema>) => { const onSubmit = async (values: z.infer<typeof STCPConfigSchema>) => {
const cfgToSubmit = { ...values, plugin: pluginConfig, type: 'stcp', name: proxyName } as STCPProxyConfig const cfgToSubmit = { ...defaultConfig, ...values, plugin: pluginConfig, type: 'stcp', name: proxyName } as STCPProxyConfig
if (!TypedProxyConfigValid(cfgToSubmit)) { if (!TypedProxyConfigValid(cfgToSubmit)) {
toast.error('Invalid configuration') toast.error('Invalid configuration')
return return
@@ -334,7 +334,7 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin) const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
const onSubmit = async (values: z.infer<typeof UDPConfigSchema>) => { const onSubmit = async (values: z.infer<typeof UDPConfigSchema>) => {
const cfgToSubmit = { ...values, plugin: pluginConfig, type: 'udp', name: proxyName } as UDPProxyConfig const cfgToSubmit = { ...defaultConfig, ...values, plugin: pluginConfig, type: 'udp', name: proxyName } as UDPProxyConfig
if (!TypedProxyConfigValid(cfgToSubmit)) { if (!TypedProxyConfigValid(cfgToSubmit)) {
toast.error('Invalid configuration') toast.error('Invalid configuration')
return return
@@ -442,7 +442,7 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin) const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => { const onSubmit = async (values: z.infer<typeof HTTPConfigSchema>) => {
const cfgToSubmit = { ...values, plugin: pluginConfig, type: 'http', name: proxyName } as HTTPProxyConfig const cfgToSubmit = { ...defaultConfig, ...values, plugin: pluginConfig, type: 'http', name: proxyName } as HTTPProxyConfig
if (!TypedProxyConfigValid(cfgToSubmit)) { if (!TypedProxyConfigValid(cfgToSubmit)) {
toast.error('Invalid configuration') toast.error('Invalid configuration')
return return

View File

@@ -1,4 +1,4 @@
"use client" 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
@@ -33,6 +33,7 @@ export type ProxyConfigMutateDialogProps = {
defaultProxyConfig?: TypedProxyConfig defaultProxyConfig?: TypedProxyConfig
defaultOriginalProxyConfig?: ProxyConfig defaultOriginalProxyConfig?: ProxyConfig
disableChangeProxyName?: boolean disableChangeProxyName?: boolean
onSuccess?: () => void
} }
export const ProxyConfigMutateDialog = ({ ...props }: ProxyConfigMutateDialogProps) => { export const ProxyConfigMutateDialog = ({ ...props }: ProxyConfigMutateDialogProps) => {
@@ -41,16 +42,14 @@ export const ProxyConfigMutateDialog = ({ ...props }: ProxyConfigMutateDialogPro
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className='w-fit'> <Button variant="outline" className="w-fit">
{t('proxy.config.create')} {t('proxy.config.create')}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className='max-h-screen overflow-auto'> <DialogContent className="max-h-[90vh] overflow-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>{t('proxy.config.create_proxy')}</DialogTitle> <DialogTitle>{t('proxy.config.create_proxy')}</DialogTitle>
<DialogDescription> <DialogDescription>{t('proxy.config.create_proxy_description')}</DialogDescription>
{t('proxy.config.create_proxy_description')}
</DialogDescription>
</DialogHeader> </DialogHeader>
<ProxyConfigMutateForm {...props} /> <ProxyConfigMutateForm {...props} />
</DialogContent> </DialogContent>
@@ -58,7 +57,13 @@ export const ProxyConfigMutateDialog = ({ ...props }: ProxyConfigMutateDialogPro
) )
} }
export const ProxyConfigMutateForm = ({ overwrite, defaultProxyConfig, defaultOriginalProxyConfig, disableChangeProxyName }: ProxyConfigMutateDialogProps) => { export const ProxyConfigMutateForm = ({
overwrite,
defaultProxyConfig,
defaultOriginalProxyConfig,
disableChangeProxyName,
onSuccess,
}: ProxyConfigMutateDialogProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [newClientID, setNewClientID] = useState<string | undefined>() const [newClientID, setNewClientID] = useState<string | undefined>()
const [newServerID, setNewServerID] = useState<string | undefined>() const [newServerID, setNewServerID] = useState<string | undefined>()
@@ -66,28 +71,30 @@ export const ProxyConfigMutateForm = ({ overwrite, defaultProxyConfig, defaultOr
const [proxyName, setProxyName] = useState<string | undefined>('') const [proxyName, setProxyName] = useState<string | undefined>('')
const [proxyType, setProxyType] = useState<ProxyType>('http') const [proxyType, setProxyType] = useState<ProxyType>('http')
const [selectedServer, setSelectedServer] = useState<Server | undefined>() const [selectedServer, setSelectedServer] = useState<Server | undefined>()
const supportedProxyTypes: ProxyType[] = ["http", "tcp", "udp"] const supportedProxyTypes: ProxyType[] = ['http', 'tcp', 'udp']
const createProxyConfigMutation = useMutation({ const createProxyConfigMutation = useMutation({
mutationKey: ['createProxyConfig', newClientID, newServerID], mutationKey: ['createProxyConfig', newClientID, newServerID],
mutationFn: () => createProxyConfig({ mutationFn: () =>
clientId: newClientID!, createProxyConfig({
serverId: newServerID!, clientId: newClientID!,
config: ObjToUint8Array({ serverId: newServerID!,
proxies: proxyConfigs config: ObjToUint8Array({
} as ClientConfig), proxies: proxyConfigs,
overwrite, } as ClientConfig),
}), overwrite,
}),
onSuccess: () => { onSuccess: () => {
toast(t('proxy.config.create_success')) toast(t('proxy.config.create_success'))
$proxyTableRefetchTrigger.set(Math.random()) $proxyTableRefetchTrigger.set(Math.random())
onSuccess?.()
}, },
onError: (e) => { onError: (e) => {
toast(t('proxy.config.create_failed'), { toast(t('proxy.config.create_failed'), {
description: JSON.stringify(e), description: JSON.stringify(e),
}) })
$proxyTableRefetchTrigger.set(Math.random()) $proxyTableRefetchTrigger.set(Math.random())
} },
}) })
useEffect(() => { useEffect(() => {
@@ -116,27 +123,39 @@ export const ProxyConfigMutateForm = ({ overwrite, defaultProxyConfig, defaultOr
<BaseSelector <BaseSelector
dataList={supportedProxyTypes.map((type) => ({ value: type, label: type }))} dataList={supportedProxyTypes.map((type) => ({ value: type, label: type }))}
value={proxyType} value={proxyType}
setValue={(value) => { setProxyType(value as ProxyType) }} setValue={(value) => {
setProxyType(value as ProxyType)
}}
/> />
{proxyConfigs && selectedServer && proxyConfigs.length > 0 && {proxyConfigs &&
proxyConfigs[0] && TypedProxyConfigValid(proxyConfigs[0]) && selectedServer &&
<div className='flex flex-row w-full overflow-auto'> proxyConfigs.length > 0 &&
<div className='flex flex-col'> proxyConfigs[0] &&
<VisitPreview server={selectedServer} typedProxyConfig={proxyConfigs[0]} /> TypedProxyConfigValid(proxyConfigs[0]) && (
<div className="flex flex-row w-full overflow-auto">
<div className="flex flex-col">
<VisitPreview server={selectedServer} typedProxyConfig={proxyConfigs[0]} />
</div>
</div> </div>
</div> )}
}
<Label>{t('proxy.config.proxy_name')} </Label> <Label>{t('proxy.config.proxy_name')} </Label>
<Input className='text-sm' defaultValue={proxyName} onChange={(e) => setProxyName(e.target.value)} disabled={disableChangeProxyName} /> <Input
{proxyName && newClientID && newServerID && <TypedProxyForm className="text-sm"
serverID={newServerID} defaultValue={proxyName}
clientID={newClientID} onChange={(e) => setProxyName(e.target.value)}
proxyName={proxyName} disabled={disableChangeProxyName}
defaultProxyConfig={proxyConfigs && proxyConfigs.length > 0 ? proxyConfigs[0] : undefined} />
clientProxyConfigs={proxyConfigs} {proxyName && newClientID && newServerID && (
setClientProxyConfigs={setProxyConfigs} <TypedProxyForm
enablePreview={false} serverID={newServerID}
/>} clientID={newClientID}
proxyName={proxyName}
defaultProxyConfig={proxyConfigs && proxyConfigs.length > 0 ? proxyConfigs[0] : undefined}
clientProxyConfigs={proxyConfigs}
setClientProxyConfigs={setProxyConfigs}
enablePreview={false}
/>
)}
<Button <Button
disabled={!TypedProxyConfigValid(proxyConfigs[0])} disabled={!TypedProxyConfigValid(proxyConfigs[0])}
onClick={() => { onClick={() => {
@@ -145,7 +164,10 @@ export const ProxyConfigMutateForm = ({ overwrite, defaultProxyConfig, defaultOr
return return
} }
createProxyConfigMutation.mutate() createProxyConfigMutation.mutate()
}} >{t('proxy.config.submit')}</Button> }}
>
{t('proxy.config.submit')}
</Button>
</> </>
) )
} }

View File

@@ -134,7 +134,7 @@ export function ProxyConfigActions({ serverID, clientID, name, row }: ProxyConfi
return ( return (
<> <>
<Dialog open={proxyMutateFormOpen} onOpenChange={setProxyMutateFormOpen}> <Dialog open={proxyMutateFormOpen} onOpenChange={setProxyMutateFormOpen}>
<DialogContent className="max-h-screen overflow-auto"> <DialogContent className="max-h-[90vh] overflow-auto">
<ProxyConfigMutateForm <ProxyConfigMutateForm
disableChangeProxyName disableChangeProxyName
defaultProxyConfig={JSON.parse(row.original.config || '{}') as TypedProxyConfig} defaultProxyConfig={JSON.parse(row.original.config || '{}') as TypedProxyConfig}

View File

@@ -26,16 +26,6 @@ export type ProxyConfigTableSchema = {
} }
export const columns: ColumnDef<ProxyConfigTableSchema>[] = [ export const columns: ColumnDef<ProxyConfigTableSchema>[] = [
{
accessorKey: 'clientID',
header: function Header() {
const { t } = useTranslation()
return t('proxy.item.client_id')
},
cell: ({ row }) => {
return <div className="font-mono text-nowrap">{row.original.originalProxyConfig.originClientId}</div>
},
},
{ {
accessorKey: 'name', accessorKey: 'name',
header: function Header() { header: function Header() {
@@ -56,6 +46,16 @@ export const columns: ColumnDef<ProxyConfigTableSchema>[] = [
return <div className="font-mono text-nowrap">{row.original.type}</div> return <div className="font-mono text-nowrap">{row.original.type}</div>
}, },
}, },
{
accessorKey: 'clientID',
header: function Header() {
const { t } = useTranslation()
return t('proxy.item.client_id')
},
cell: ({ row }) => {
return <div className="font-mono text-nowrap">{row.original.originalProxyConfig.originClientId}</div>
},
},
{ {
accessorKey: 'serverID', accessorKey: 'serverID',
header: function Header() { header: function Header() {

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,219 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { ClientSelector } from '../base/client-selector'
import { Cpu, Download, Loader2, Trash } from 'lucide-react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { getWorkerStatus, installWorkerd } from '@/api/worker'
import { Client } from '@/lib/pb/common'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Badge } from '@/components/ui/badge'
import { toast } from 'sonner'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface ClientDeploymentProps {
workerId: string
deployedClientIDs: string[]
setDeployedClientIDs: (ids: string[]) => void
clients?: Client[]
}
export function ClientDeployment({
workerId,
deployedClientIDs,
setDeployedClientIDs,
clients = [],
}: ClientDeploymentProps) {
const { t } = useTranslation()
const [dialogOpen, setDialogOpen] = useState(false)
const [selectedClientId, setSelectedClientId] = useState('')
const [downloadUrl, setDownloadUrl] = useState('')
const { data: statusResp } = useQuery({
queryKey: ['workerStatus', workerId],
queryFn: () => getWorkerStatus({ workerId }),
enabled: !!workerId,
refetchInterval: 10000,
})
const installWorkerdMutation = useMutation({
mutationFn: installWorkerd,
onSuccess: () => {
toast.success(t('worker.client_install_workerd.success'))
},
onError: () => {
toast.error(t('worker.client_install_workerd.error'))
},
})
const statusMap = statusResp?.workerStatus || {}
const handleAddClient = () => {
if (selectedClientId && !deployedClientIDs.includes(selectedClientId)) {
setDeployedClientIDs([...deployedClientIDs, selectedClientId])
setSelectedClientId('')
setDialogOpen(false)
}
}
const handleRemoveClient = (clientId: string) => {
setDeployedClientIDs(deployedClientIDs.filter((id) => id !== clientId))
}
function getStatusInfo(status?: string): {
variant: 'outline' | 'default' | 'secondary' | 'destructive'
text: string
} {
if (status === 'running') {
return { variant: 'default', text: t('worker.status_running') }
} else if (status === 'stopped') {
return { variant: 'destructive', text: t('worker.status_stopped') }
} else if (status === 'error') {
return { variant: 'secondary', text: t('worker.status_error') }
} else {
return { variant: 'outline', text: t('worker.status_unknown') }
}
}
return (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center">
<Cpu className="h-5 w-5 mr-2 text-muted-foreground" />
{t('worker.deploy.title')}
</CardTitle>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button size="sm" variant="outline" className="h-8 text-xs">
{t('worker.deploy.add_client')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('worker.deploy.select_client')}</DialogTitle>
<DialogDescription>{t('worker.deploy.client_description')}</DialogDescription>
</DialogHeader>
<div className="py-4">
<ClientSelector clientID={selectedClientId} setClientID={setSelectedClientId} />
</div>
<DialogFooter>
<Button onClick={handleAddClient} disabled={!selectedClientId}>
{t('common.add')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-2">
{deployedClientIDs.length === 0 ? (
<div className="text-sm text-muted-foreground flex items-center justify-center py-6 border border-dashed rounded-md">
{t('worker.deploy.no_clients')}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{deployedClientIDs.map((clientId) => {
const clientStatus = statusMap[clientId]
const { variant, text } = getStatusInfo(clientStatus)
const client = clients.find((c) => c.id === clientId)
return (
<div
key={clientId}
className="group flex flex-col overflow-hidden rounded-md border hover:border-primary/40 hover:shadow-sm transition-all duration-200"
>
<div className="flex items-center justify-between bg-muted/30 px-3 py-2 border-b">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<h3 className="font-semibold text-sm truncate max-w-[200px] md:max-w-[300px]">
{client?.originClientId || clientId}
</h3>
</TooltipTrigger>
<TooltipContent>{client?.originClientId || clientId}</TooltipContent>
</Tooltip>
</TooltipProvider>
<Badge variant={variant} className="font-normal whitespace-nowrap">
{text}
</Badge>
</div>
<div className="px-3 py-2 flex-grow">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<span className="text-xs font-medium text-muted-foreground mr-1">ID:</span>
<span className="font-mono text-xs truncate max-w-[200px]">{clientId}</span>
</div>
</TooltipTrigger>
<TooltipContent>{clientId}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="bg-muted/10 px-3 py-1.5 flex items-center justify-end border-t">
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7">
<Download className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('worker.client_install_workerd.title')}</DialogTitle>
<DialogDescription>{t('worker.client_install_workerd.description')}</DialogDescription>
</DialogHeader>
<Label>{t('worker.client_install_workerd.download_url')}</Label>
<Input
placeholder={t('worker.client_install_workerd.placeholder')}
defaultValue={downloadUrl}
onChange={(e) => setDownloadUrl(e.target.value)}
/>
<DialogFooter className="pt-4">
<Button
onClick={() => {
installWorkerdMutation.mutate({ clientId })
}}
disabled={installWorkerdMutation.isPending}
>
{installWorkerdMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t('worker.client_install_workerd.button')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-red-500 hover:text-red-600 hover:bg-red-50"
onClick={() => handleRemoveClient(clientId)}
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
)
})}
</div>
)}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,22 @@
'use client'
import React from 'react'
import dynamic from 'next/dynamic'
// 动态加载编辑器组件
const MonacoEditor = dynamic(() => import('@/components/base/monaco').then((m) => m.WorkerEditor), {
ssr: false,
})
interface WorkerCodeEditorProps {
code: string
onChange: (code: string) => void
}
export function WorkerCodeEditor({ code, onChange }: WorkerCodeEditorProps) {
return (
<div className="h-full w-full">
<MonacoEditor code={code} onChange={onChange} />
</div>
)
}

View File

@@ -0,0 +1,127 @@
'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSearchParams } from 'next/navigation'
import { useQuery, useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
import { useTranslation } from 'react-i18next'
import { getWorker, updateWorker } from '@/api/worker'
import { UpdateWorkerRequest } from '@/lib/pb/api_client'
import { Worker } from '@/lib/pb/common'
import { Button } from '@/components/ui/button'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { WorkerInfoCard } from './info_card'
import { WorkerIngress } from './ingress_section'
import { ClientDeployment } from './client_deploy'
import { WorkerCodeEditor } from './code_editor'
import { WorkerTemplateEditor } from './template_editor'
export default function WorkerEdit() {
const router = useRouter()
const params = useSearchParams()
const workerId = params.get('workerId')!
const { t } = useTranslation()
// 本地状态
const [worker, setWorker] = useState<Worker>({} as Worker)
const [code, setCode] = useState('')
const [template, setTemplate] = useState('')
const [deployedClientIDs, setDeployedClientIDs] = useState<string[]>([])
// 获取 Worker
const { data: resp, refetch: refetchWorker } = useQuery({
queryKey: ['getWorker', workerId],
queryFn: () => getWorker({ workerId }),
enabled: !!workerId,
})
useEffect(() => {
if (resp?.worker) {
setWorker(resp.worker)
setCode(resp.worker.code ?? '')
setTemplate(resp.worker.configTemplate ?? '')
// @ts-ignore
setDeployedClientIDs(resp.clients.map((client) => client.id).filter((id) => id !== undefined) || [])
}
}, [resp])
// 更新 Worker
const updateMut = useMutation({
mutationFn: () => {
const req: UpdateWorkerRequest = {
clientIds: deployedClientIDs,
worker: {
...worker,
code,
configTemplate: template,
},
}
return updateWorker(req)
},
onSuccess: () => {
toast.success(t('worker.edit.save_success'))
refetchWorker()
},
onError: (e) => toast.error(`${t('worker.edit.save_error')}: ${e.message}`),
})
return (
<div className="container p-4 mx-auto space-y-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h1 className="text-xl md:text-2xl font-semibold flex flex-row gap-1 sm:items-center">
<span>{t('worker.edit.title')}</span>
<span className="font-mono text-gray-500">{worker?.name}</span>
</h1>
<div className="flex gap-2 w-full sm:w-auto">
<Button variant="outline" onClick={() => router.back()} className="flex-1 sm:flex-none">
{t('common.back')}
</Button>
<Button onClick={() => updateMut.mutate()} disabled={updateMut.isPending} className="flex-1 sm:flex-none">
{updateMut.isPending ? t('common.saving') : t('common.save')}
</Button>
</div>
</div>
<div className="space-y-4">
<Tabs defaultValue="info" className="w-full">
<TabsList className="w-full">
<TabsTrigger value="info" className="flex-1">
{t('worker.edit.info_tab')}
</TabsTrigger>
<TabsTrigger value="code" className="flex-1">
{t('worker.edit.code_tab')}
</TabsTrigger>
<TabsTrigger value="template" className="flex-1">
{t('worker.edit.template_tab')}
</TabsTrigger>
</TabsList>
<TabsContent value="info" className="overflow-hidden space-y-4 mt-4">
<WorkerInfoCard worker={worker} onChange={setWorker} clients={resp?.clients} />
{/* <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> */}
<ClientDeployment
workerId={workerId}
deployedClientIDs={deployedClientIDs}
setDeployedClientIDs={setDeployedClientIDs}
clients={resp?.clients}
/>
<WorkerIngress workerId={workerId} refetchWorker={refetchWorker} clients={resp?.clients} />
{/* </div> */}
</TabsContent>
<TabsContent value="code" className="h-[calc(100vh-240px)] border rounded-md overflow-hidden mt-4">
<WorkerCodeEditor code={code} onChange={setCode} />
</TabsContent>
<TabsContent value="template" className="h-[calc(100vh-240px)] border rounded-md overflow-hidden mt-4">
<WorkerTemplateEditor content={template} onChange={setTemplate} />
</TabsContent>
</Tabs>
</div>
</div>
)
}

View File

@@ -0,0 +1,104 @@
'use client'
import React from 'react'
import { Client, Worker } from '@/lib/pb/common'
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { useTranslation } from 'react-i18next'
import { WorkerStatus } from './worker_status'
import { useQuery } from '@tanstack/react-query'
import { getWorkerIngress } from '@/api/worker'
import { InfoIcon } from 'lucide-react'
// import { Textarea } from '@/components/ui/textarea'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
interface WorkerInfoCardProps {
worker: Worker
onChange: (worker: Worker) => void
clients?: Client[]
}
export function WorkerInfoCard({ worker, onChange, clients = [] }: WorkerInfoCardProps) {
const { t } = useTranslation()
// 获取 Worker Ingress 用于状态统计
const { data: ingressResp } = useQuery({
queryKey: ['getWorkerIngress', worker.workerId],
queryFn: () => getWorkerIngress({ workerId: worker.workerId || '' }),
enabled: !!worker.workerId,
})
return (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center">
<InfoIcon className="h-5 w-5 mr-2 text-muted-foreground" />
{t('worker.info.basic_info')}
</CardTitle>
<WorkerStatus workerId={worker.workerId || ''} clients={clients} />
</div>
<CardDescription className="text-xs">{t('worker.info.info_description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-0">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-xs font-medium">{t('worker.info.worker_id')}</Label>
<div className="flex">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Input value={worker.workerId || ''} readOnly className="bg-muted font-mono text-sm h-9" />
</TooltipTrigger>
<TooltipContent>{worker.workerId || ''}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs font-medium">{t('worker.info.worker_name')}</Label>
<div className="flex">
<Input
value={worker.name || ''}
onChange={(e) => onChange({ ...worker, name: e.target.value })}
className="h-9 text-sm"
placeholder={t('worker.info.name_placeholder')}
/>
</div>
</div>
</div>
{/* 暂不实现 */}
{/* <div className="space-y-2">
<Label className="text-xs font-medium">{t('worker.info.description')}</Label>
<Textarea
value={worker.description || ''}
onChange={(e) => onChange({ ...worker, description: e.target.value })}
placeholder={t('worker.info.description_placeholder')}
className="resize-none h-20 text-sm"
/>
</div> */}
<div className="flex items-center justify-between rounded-md bg-muted/50 p-3 border">
<div className="space-y-1">
<h4 className="text-sm font-medium">{t('worker.info.resources')}</h4>
<div className="flex space-x-4 text-xs text-muted-foreground">
<div>
{t('worker.info.clients')}: <span className="font-medium">{clients.length}</span>
</div>
<div>
{t('worker.info.ingresses')}:{' '}
<span className="font-medium">{ingressResp?.proxyConfigs?.length || 0}</span>
</div>
</div>
</div>
{/* 暂不实现 */}
{/* <div className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-md font-medium">
{worker.version || t('worker.info.unknown_version')}
</div> */}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,363 @@
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useQuery, useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { createWorkerIngress, getWorkerIngress } from '@/api/worker'
import { Client, ProxyConfig } from '@/lib/pb/common'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { ProxyConfigMutateForm } from '../proxy/mutate_proxy_config'
import { Loader2, Settings, Trash, Network } from 'lucide-react'
import { TypedProxyConfig } from '@/types/proxy'
import { ClientSelector } from '../base/client-selector'
import { ServerSelector } from '../base/server-selector'
import { Label } from '@/components/ui/label'
import { deleteProxyConfig, getProxyConfig } from '@/api/proxy'
import { useStore } from '@nanostores/react'
import { $proxyTableRefetchTrigger } from '@/store/refetch-trigger'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Badge } from '@/components/ui/badge'
interface WorkerIngressProps {
workerId: string
refetchWorker: () => void
clients?: Client[]
}
// 创建 Worker Ingress 表单组件
const CreateWorkerIngressForm = ({
workerId,
onSuccess,
clients,
}: {
workerId: string
onSuccess: () => void
clients?: Client[]
}) => {
const { t } = useTranslation()
const [clientId, setClientId] = useState<string>('')
const [serverId, setServerId] = useState<string>('')
const createWorkerIngressMutation = useMutation({
mutationFn: createWorkerIngress,
onSuccess: () => {
toast.success(t('worker.ingress.create_success'))
onSuccess()
},
onError: (error) => {
toast.error(t('worker.ingress.create_failed'), {
description: error instanceof Error ? error.message : String(error),
})
},
})
const handleSubmit = () => {
if (!clientId) {
toast.error(t('worker.ingress.client_required'))
return
}
if (!serverId) {
toast.error(t('worker.ingress.server_required'))
return
}
createWorkerIngressMutation.mutate({
clientId,
serverId,
workerId,
})
}
return (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="client-select">{t('worker.ingress.select_client')}</Label>
<ClientSelector setClientID={setClientId} clientID={clientId} clients={clients} />
</div>
<div className="space-y-2">
<Label htmlFor="server-select">{t('worker.ingress.select_server')}</Label>
<ServerSelector setServerID={setServerId} serverID={serverId} />
</div>
<DialogFooter>
<Button disabled={!clientId || !serverId || createWorkerIngressMutation.isPending} onClick={handleSubmit}>
{createWorkerIngressMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('worker.ingress.creating')}
</>
) : (
t('worker.ingress.create_submit')
)}
</Button>
</DialogFooter>
</div>
)
}
function ProxyStatusBadge({
clientId,
serverId,
proxyName,
}: {
clientId?: string
serverId?: string
proxyName?: string
}) {
const { t } = useTranslation()
const refetchTrigger = useStore($proxyTableRefetchTrigger)
const { data } = useQuery({
queryKey: ['getProxyConfig', clientId, serverId, proxyName, refetchTrigger],
queryFn: () => {
return getProxyConfig({
clientId,
serverId,
name: proxyName,
})
},
enabled: !!clientId && !!serverId && !!proxyName,
refetchInterval: 10000,
})
function getStatusInfo(status: string): {
color: string
text: string
variant: 'outline' | 'default' | 'secondary' | 'destructive'
} {
switch (status) {
case 'new':
return { color: 'bg-blue-100 border-blue-400', text: t('status.new'), variant: 'secondary' }
case 'wait start':
return { color: 'bg-yellow-100 border-yellow-400', text: t('status.wait_start'), variant: 'secondary' }
case 'start error':
return { color: 'bg-red-100 border-red-400', text: t('status.start_error'), variant: 'destructive' }
case 'running':
return { color: 'bg-green-100 border-green-400', text: t('status.running'), variant: 'default' }
case 'check failed':
return { color: 'bg-orange-100 border-orange-400', text: t('status.check_failed'), variant: 'secondary' }
case 'error':
return { color: 'bg-red-100 border-red-400', text: t('status.error'), variant: 'destructive' }
default:
return { color: 'bg-gray-100 border-gray-400', text: t('status.unknown'), variant: 'outline' }
}
}
const status = data?.workingStatus?.status || 'unknown'
const { text, variant } = getStatusInfo(status)
return (
<Badge variant={variant} className={'font-normal whitespace-nowrap'}>
{text}
</Badge>
)
}
export function WorkerIngress({ workerId, refetchWorker, clients }: WorkerIngressProps) {
const { t } = useTranslation()
// 获取 Worker Ingress
const { data: ingresses, refetch: refetchIngresses } = useQuery({
queryKey: ['getWorkerIngress', workerId],
queryFn: () => getWorkerIngress({ workerId }),
enabled: !!workerId,
})
const handleIngressCreated = () => {
refetchIngresses()
refetchWorker()
}
return (
<Card className="shadow-sm">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center">
<Network className="h-5 w-5 mr-2 text-muted-foreground" />
{t('worker.ingress.title')}
</CardTitle>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline" className="h-8 text-xs">
{t('worker.ingress.create')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('worker.ingress.create_title')}</DialogTitle>
<DialogDescription>{t('worker.ingress.create_description')}</DialogDescription>
</DialogHeader>
<CreateWorkerIngressForm workerId={workerId} onSuccess={handleIngressCreated} clients={clients} />
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-2">
{!ingresses || !ingresses.proxyConfigs || ingresses.proxyConfigs.length === 0 ? (
<div className="text-sm text-muted-foreground flex items-center justify-center py-6 border border-dashed rounded-md">
{t('worker.ingress.no_ingress')}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 rounded-md">
{ingresses.proxyConfigs.map((ingress: ProxyConfig) => (
<div
key={ingress.id}
className="group overflow-hidden rounded-md border hover:border-primary/40 hover:shadow-sm transition-all duration-200 flex flex-col"
>
<div className="flex items-center justify-between bg-muted/30 px-3 py-2 border-b">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<h3 className="font-semibold text-sm truncate max-w-[200px] md:max-w-[300px]">
{ingress.name}
</h3>
</TooltipTrigger>
<TooltipContent>{ingress.name}</TooltipContent>
</Tooltip>
</TooltipProvider>
<ProxyStatusBadge
clientId={ingress.clientId}
serverId={ingress.serverId}
proxyName={ingress.name}
/>
</div>
<div className="px-3 py-2 flex-grow">
<div className="flex flex-col space-y-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<span className="text-xs font-medium text-muted-foreground mr-1">Server:</span>
<span className="font-mono text-xs truncate max-w-[150px] md:max-w-[200px]">
{ingress.serverId}
</span>
</div>
</TooltipTrigger>
<TooltipContent>{ingress.serverId}</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<span className="text-xs font-medium text-muted-foreground mr-1">Client:</span>
<span className="font-mono text-xs truncate max-w-[150px] md:max-w-[200px]">
{ingress.originClientId}
</span>
</div>
</TooltipTrigger>
<TooltipContent>{ingress.originClientId}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div className="bg-muted/10 px-3 py-1.5 flex items-center justify-end gap-1 border-t">
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7">
<Settings className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{t('worker.ingress.edit')}</DialogTitle>
</DialogHeader>
<ProxyConfigMutateForm
disableChangeProxyName
defaultProxyConfig={JSON.parse(ingress.config || '{}') as TypedProxyConfig}
overwrite={true}
defaultOriginalProxyConfig={ingress}
onSuccess={handleIngressCreated}
/>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-red-500 hover:text-red-600 hover:bg-red-50"
>
<Trash className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('worker.ingress.delete.title')}</DialogTitle>
<DialogDescription>{t('worker.ingress.delete.description')}</DialogDescription>
</DialogHeader>
<IngressDeleteForm
clientId={ingress.clientId}
serverId={ingress.serverId}
proxyName={ingress.name}
onSuccess={handleIngressCreated}
/>
</DialogContent>
</Dialog>
</div>
</div>
))}
</div>
)}
</div>
</CardContent>
</Card>
)
}
export const IngressDeleteForm = ({
clientId,
serverId,
proxyName,
onSuccess,
}: {
clientId?: string
serverId?: string
proxyName?: string
onSuccess?: () => void
}) => {
const { t } = useTranslation()
// 删除 Ingress 对应的 ProxyConfig
const deleteProxyConfigMutation = useMutation({
mutationFn: deleteProxyConfig,
onSuccess: () => {
onSuccess?.()
toast(t('worker.ingress.delete_success'), {
description: t('worker.ingress.delete_description', { proxyName }),
})
},
})
return (
<DialogFooter className="pt-4">
<Button
variant={'destructive'}
onClick={() =>
deleteProxyConfigMutation.mutate({
clientId,
serverId,
name: proxyName,
})
}
disabled={deleteProxyConfigMutation.isPending}
>
{deleteProxyConfigMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t('worker.ingress.delete.button')}
</Button>
</DialogFooter>
)
}

View File

@@ -0,0 +1,25 @@
'use client'
import React, { useLayoutEffect } from 'react'
import Editor, { loader } from '@monaco-editor/react'
loader.config({
paths: {
vs: 'https://fastly.jsdelivr.net/npm/monaco-editor@0.36.1/min/vs',
},
})
export interface TemplateEditorProps {
content: string
onChange: (value: string) => void
}
export function TemplateEditor({ content, onChange }: TemplateEditorProps) {
useLayoutEffect(() => {}, [])
return (
<div className="h-full">
<Editor height="100%" defaultLanguage="capnp" value={content} onChange={(v) => onChange(v ?? '')} />
</div>
)
}

View File

@@ -0,0 +1,23 @@
'use client'
import React from 'react'
import dynamic from 'next/dynamic'
// 动态加载模板编辑器组件
const TemplateEditorComponent = dynamic(
() => import('@/components/worker/template_edit').then((m) => m.TemplateEditor),
{ ssr: false },
)
interface WorkerTemplateEditorProps {
content: string
onChange: (content: string) => void
}
export function WorkerTemplateEditor({ content, onChange }: WorkerTemplateEditorProps) {
return (
<div className="h-full w-full">
<TemplateEditorComponent content={content} onChange={onChange} />
</div>
)
}

View File

@@ -0,0 +1,116 @@
import React from 'react'
import { useMutation } from '@tanstack/react-query'
import { z } from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { createWorker } from '@/api/worker'
import { CreateWorkerRequest } from '@/lib/pb/api_client'
import { toast } from 'sonner'
import { useTranslation } from 'react-i18next'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Form, FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { ClientSelector } from '../base/client-selector'
const CreateWorkerSchema = z.object({
clientId: z.string().min(1, 'worker.createClientRequired'),
name: z.string().min(1, 'worker.createNameRequired'),
})
type CreateWorkerValues = z.infer<typeof CreateWorkerSchema>
export interface CreateWorkerDialogProps {
refetchTrigger: React.Dispatch<React.SetStateAction<string>>
}
export const CreateWorkerDialog: React.FC<CreateWorkerDialogProps> = ({ refetchTrigger }) => {
const { t } = useTranslation()
const [open, setOpen] = React.useState(false)
const form = useForm<CreateWorkerValues>({
resolver: zodResolver(CreateWorkerSchema),
defaultValues: {
clientId: '',
name: '',
},
})
const { mutate, isPending } = useMutation({
mutationFn: (values: CreateWorkerValues) => {
const req: CreateWorkerRequest = {
clientId: values.clientId,
worker: { name: values.name },
}
return createWorker(req)
},
onSuccess: () => {
toast.success(t('worker.create.success'))
form.reset()
setOpen(false)
refetchTrigger(new Date().toISOString())
},
onError: (err: any) => {
toast(err?.message || t('worker.create'))
},
})
const onSubmit = (values: CreateWorkerValues) => {
mutate(values)
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">{t('worker.create.button')}</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('worker.create.title')}</DialogTitle>
<DialogDescription>{t('worker.create.description')}</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="clientId"
render={({ field }) => (
<FormItem>
<FormLabel>{t('worker.create.clientIdLabel')}</FormLabel>
<FormControl>
<ClientSelector clientID={field.value} setClientID={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('worker.create.nameLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('worker.create.namePlaceholder')} {...field} />
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button type="submit" disabled={isPending}>
{isPending ? t('worker.create.creating') : t('worker.create.submit')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,183 @@
import { MoreHorizontal, ArrowUpRight } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogClose,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { removeWorker, getWorker, getWorkerIngress, getWorkerStatus } from '@/api/worker'
import { $workerTableRefetchTrigger } from '@/store/refetch-trigger'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/router'
import { Worker } from '@/lib/pb/common'
import { toast } from 'sonner'
import { useMutation, useQuery } from '@tanstack/react-query'
import { WorkerStatus } from './worker_status'
import { ColumnDef, Row } from '@tanstack/react-table'
export type WorkerTableSchema = {
workerId: string
name: string
userId: number
tenantId: number
socketAddress: string
origin: Worker
}
export const columns: ColumnDef<WorkerTableSchema>[] = [
{
accessorKey: 'name',
header: ({ column }: { column: ColumnDef<WorkerTableSchema> }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = useTranslation()
return t('worker.columns.name')
},
cell: ({ row }: { row: Row<WorkerTableSchema> }) => {
const worker = row.original
// eslint-disable-next-line react-hooks/rules-of-hooks
const router = useRouter()
return (
<div className="flex items-center">
<span className="mr-2 font-medium">{worker.name || worker.workerId}</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 rounded-full"
onClick={() =>
router.push({
pathname: '/worker-edit',
query: { workerId: worker.workerId },
})
}
>
<ArrowUpRight className="h-3.5 w-3.5" />
</Button>
</div>
)
},
},
{
id: 'status',
header: ({ column }: { column: ColumnDef<WorkerTableSchema> }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = useTranslation()
return t('worker.columns.status')
},
cell: ({ row }: { row: Row<WorkerTableSchema> }) => {
const workerId = row.getValue('workerId') as string
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: workerData } = useQuery({
queryKey: ['getWorker', workerId],
queryFn: () => getWorker({ workerId }),
enabled: !!workerId,
})
return (
<div className="flex justify-start">
<WorkerStatus workerId={workerId} clients={workerData?.clients || []} />
</div>
)
},
},
{
accessorKey: 'workerId',
header: ({ column }: { column: ColumnDef<WorkerTableSchema> }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { t } = useTranslation()
return t('worker.columns.id')
},
cell: ({ row }: { row: Row<WorkerTableSchema> }) => {
return <div className="font-mono text-sm text-nowarp whitespace-nowrap">{row.getValue('workerId')}</div>
},
},
{
id: 'actions',
cell: ({ row }: { row: Row<WorkerTableSchema> }) => {
const worker = row.original
return <WorkerActions worker={worker} />
},
},
]
interface WorkerActionsProps {
worker: WorkerTableSchema
}
export const WorkerActions: React.FC<WorkerActionsProps> = ({ worker }) => {
const { t } = useTranslation()
const router = useRouter()
const del = useMutation({
mutationFn: () => removeWorker({ workerId: worker.workerId }),
onSuccess: () => {
toast.success(t('worker.actions_menu.delete') + t('common.success'))
$workerTableRefetchTrigger.set(Math.random())
},
onError: (err: any) => {
toast(t('common.failed'), { description: err.message })
$workerTableRefetchTrigger.set(Math.random())
},
})
return (
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t('worker.actions_menu.title')}</DropdownMenuLabel>
<DropdownMenuItem
onClick={() =>
router.push({
pathname: '/worker-edit',
query: { workerId: worker.workerId },
})
}
>
{t('worker.actions_menu.edit')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DialogTrigger asChild>
<DropdownMenuItem className="text-destructive">{t('worker.actions_menu.delete')}</DropdownMenuItem>
</DialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('worker.delete.title')}</DialogTitle>
<DialogDescription>{t('worker.delete.description', { name: worker.name })}</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" className="mr-2">
{t('common.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="destructive" onClick={() => del.mutate()}>
{t('worker.delete.confirm')}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,77 @@
import React from 'react'
import { useRouter } from 'next/router'
import { useQuery, useMutation, keepPreviousData } from '@tanstack/react-query'
import {
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
useReactTable,
SortingState,
PaginationState,
ColumnFiltersState,
} from '@tanstack/react-table'
import { useStore } from '@nanostores/react'
import { listWorkers } from '@/api/worker'
import { Worker as PbWorker } from '@/lib/pb/common'
import { DataTable } from '../base/data_table'
import { WorkerTableSchema, columns as workerColumnsDef } from './worker_item'
import { $workerTableRefetchTrigger } from '@/store/refetch-trigger'
export interface WorkerListProps {
initialWorkers: PbWorker[]
initialTotal: number
triggerRefetch?: string
keyword?: string
}
export const WorkerList: React.FC<WorkerListProps> = ({ initialWorkers, initialTotal, triggerRefetch, keyword }) => {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [{ pageIndex, pageSize }, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const globalTrigger = useStore($workerTableRefetchTrigger)
const fetchOptions = { pageIndex, pageSize, triggerRefetch, globalTrigger, keyword }
const { data, isFetching } = useQuery({
queryKey: ['listWorkers', fetchOptions],
queryFn: () =>
listWorkers({
page: pageIndex + 1,
pageSize,
keyword,
}),
placeholderData: keepPreviousData,
})
const dataRows: WorkerTableSchema[] =
data?.workers.map((w) => ({
workerId: w.workerId ?? '',
name: w.name ?? '',
userId: w.userId ?? 0,
tenantId: w.tenantId ?? 0,
socketAddress: w.socket?.address ?? '',
origin: w,
})) ?? []
const table = useReactTable({
data: dataRows,
columns: workerColumnsDef,
state: { sorting, pagination: { pageIndex, pageSize }, columnFilters },
manualPagination: true,
pageCount: Math.ceil((data?.total ?? 0) / pageSize),
onSortingChange: setSorting,
onPaginationChange: setPagination,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
return <DataTable table={table} columns={workerColumnsDef} />
}

View File

@@ -0,0 +1,233 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query'
import { getWorkerStatus, getWorkerIngress } from '@/api/worker'
import { getProxyConfig } from '@/api/proxy'
import { useStore } from '@nanostores/react'
import { $proxyTableRefetchTrigger } from '@/store/refetch-trigger'
import { Client, ProxyConfig } from '@/lib/pb/common'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Badge } from '@/components/ui/badge'
import { Cpu, Network } from 'lucide-react'
interface WorkerStatusProps {
workerId: string
clients?: Client[]
compact?: boolean
}
export function WorkerStatus({ workerId, clients = [], compact = false }: WorkerStatusProps) {
const { t } = useTranslation()
const refetchTrigger = useStore($proxyTableRefetchTrigger)
// 获取 Worker 状态
const { data: statusResp } = useQuery({
queryKey: ['workerStatus', workerId],
queryFn: () => getWorkerStatus({ workerId }),
enabled: !!workerId,
refetchInterval: 10000,
})
// 获取 Worker Ingress
const { data: ingressResp } = useQuery({
queryKey: ['getWorkerIngress', workerId],
queryFn: () => getWorkerIngress({ workerId }),
enabled: !!workerId,
})
// 统计部署状态
const clientStatuses = statusResp?.workerStatus || {}
const deployedClients = clients || []
const totalClients = deployedClients.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
// 查询所有入口的状态
const ingressStatuses = useQuery({
queryKey: ['getIngressStatuses', workerId, ingresses.map((i: ProxyConfig) => i.id).join(','), refetchTrigger],
queryFn: async () => {
const statuses: Record<string, string> = {}
await Promise.all(
ingresses.map(async (ingress: ProxyConfig) => {
try {
const proxyStatus = await getProxyConfig({
clientId: ingress.clientId,
serverId: ingress.serverId,
name: ingress.name,
})
statuses[ingress.id || ''] = proxyStatus?.workingStatus?.status || 'unknown'
} catch (e) {
statuses[ingress.id || ''] = 'error'
}
}),
)
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),
).length
// 计算总体状态
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) {
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 { variant, text, color } = getOverallStatus()
// 生成客户端资源状态指示器
const renderClientIndicators = () => {
if (totalClients === 0) return null
return (
<div className="flex items-center group relative">
<div className="flex items-center space-x-1 rounded-md bg-muted/30 px-1 py-0.5 border border-muted">
<div className="flex space-x-0.5">
{Array.from({ length: Math.min(totalClients, 3) }).map((_, i) => (
<div
key={`client-${i}`}
className={`h-2.5 w-2.5 rounded-sm ${
i < runningClients ? 'bg-green-500' : i < runningClients + errorClients ? 'bg-red-500' : 'bg-gray-300'
}`}
/>
))}
</div>
<span className="text-xs text-muted-foreground">
<Cpu className="w-3 h-3 min-w-3 min-h-3 max-w-3 max-h-3" />
</span>
{totalClients > 3 && <span className="text-xs text-muted-foreground">+{totalClients - 3}</span>}
</div>
</div>
)
}
// 生成入口资源状态指示器
const renderIngressIndicators = () => {
if (totalIngresses === 0) return null
return (
<div className="flex items-center group relative">
<div className="flex items-center space-x-1 rounded-md bg-muted/30 px-1 py-0.5 border border-muted">
<div className="flex space-x-0.5">
{Array.from({ length: Math.min(totalIngresses, 3) }).map((_, i) => (
<div
key={`ingress-${i}`}
className={`h-2.5 w-2.5 rounded-sm ${
i < runningIngresses
? 'bg-green-500'
: i < runningIngresses + errorIngresses
? 'bg-red-500'
: 'bg-gray-300'
}`}
/>
))}
</div>
<span className="text-xs text-muted-foreground">
<Network className="w-3 h-3 min-w-3 min-h-3 max-w-3 max-h-3" />
</span>
{totalIngresses > 3 && <span className="text-xs text-muted-foreground">+{totalIngresses - 3}</span>}
</div>
</div>
)
}
if (compact) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="flex items-center space-x-1">
<div
className={`h-2 w-2 rounded-sm ${
variant === 'default'
? 'bg-green-500'
: variant === 'destructive'
? 'bg-red-500'
: variant === 'warning'
? 'bg-amber-500'
: variant === 'secondary'
? 'bg-blue-500'
: 'bg-gray-300'
}`}
/>
</TooltipTrigger>
<TooltipContent>
<div className="space-y-1">
<p className="text-sm font-medium">{text}</p>
<div className="text-xs">
<div className="flex items-center space-x-1 font-mono">
{t('worker.status.clients')}: {runningClients}/{totalClients} {t('worker.status.running')}
</div>
<div className="flex items-center space-x-1 font-mono">
{t('worker.status.ingresses')}: {runningIngresses}/{totalIngresses} {t('worker.status.running')}
</div>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-2">
<div className="flex space-x-1">
{renderIngressIndicators()}
{renderClientIndicators()}
</div>
<Badge className={`px-2 py-0.5 ${color} whitespace-nowrap`}>{text}</Badge>
</div>
</TooltipTrigger>
<TooltipContent>
<div className="space-y-2">
<p className="text-sm font-medium">{text}</p>
<div className="space-y-1 text-xs">
<div className="flex items-center space-x-1 font-mono">
{t('worker.status.clients')}: {runningClients}/{totalClients} {t('worker.status.running')}
</div>
<div className="flex items-center space-x-1 font-mono">
{t('worker.status.ingresses')}: {runningIngresses}/{totalIngresses} {t('worker.status.running')}
</div>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

View File

@@ -7,6 +7,7 @@ import {
ChartNetworkIcon, ChartNetworkIcon,
Scroll, Scroll,
Cable, Cable,
SquareFunction,
} from "lucide-react" } from "lucide-react"
import { TbBuildingTunnel } from "react-icons/tb" import { TbBuildingTunnel } from "react-icons/tb"
@@ -61,4 +62,9 @@ export const getNavItems = (t: any) => [
url: "/console", url: "/console",
icon: SquareTerminal, icon: SquareTerminal,
}, },
{
title: t('nav.workers'),
url: "/workers",
icon: SquareFunction,
},
] ]

View File

@@ -177,6 +177,13 @@
"register": "Register", "register": "Register",
"userInfo": "User Information", "userInfo": "User Information",
"logout": "Logout", "logout": "Logout",
"add": "Add",
"back": "Back",
"saving": "Saving...",
"save": "Save",
"success": "Success",
"failed": "Failed",
"cancel": "Cancel",
"clientType": "Client Type", "clientType": "Client Type",
"disconnect": "Disconnect", "disconnect": "Disconnect",
"connect": "Connect" "connect": "Connect"
@@ -299,9 +306,9 @@
"title": "Tunnel Operation" "title": "Tunnel Operation"
}, },
"item": { "item": {
"client_id": "Client ID",
"proxy_name": "Tunnel Name", "proxy_name": "Tunnel Name",
"proxy_type": "Tunnel Type", "proxy_type": "Tunnel Type",
"client_id": "Client ID",
"server_id": "Server ID", "server_id": "Server ID",
"status": "Status", "status": "Status",
"visit_preview": "Visit Preview" "visit_preview": "Visit Preview"
@@ -470,6 +477,113 @@
"cancel": "Cancel", "cancel": "Cancel",
"confirm": "Confirm" "confirm": "Confirm"
}, },
"worker": {
"client_install_workerd": {
"success": "Workerd installed successfully",
"error": "Failed to install Workerd",
"title": "Install Workerd",
"description": "Install Workerd binary from url to Client",
"download_url": "Workerd Binary Download URL",
"placeholder": "Enter the download URL",
"button": "Install"
},
"status_running": "Running",
"status_stopped": "Stopped",
"status_error": "Error",
"status_unknown": "Unknown",
"deploy": {
"title": "Deploy",
"add_client": "Add Deployment",
"select_client": "Select Client",
"client_description": "Select a client to deploy",
"no_clients": "No clients available"
},
"edit": {
"save_success": "Changes saved successfully",
"save_error": "Failed to save changes",
"title": "Edit Worker",
"info_tab": "Worker Info",
"code_tab": "Code",
"template_tab": "Template"
},
"info": {
"basic_info": "Basic Information",
"info_description": "You can edit the basic information",
"worker_id": "Worker ID",
"worker_name": "Worker Name",
"name_placeholder": "Enter worker name",
"resources": "Resources",
"clients": "Deployments",
"ingresses": "Ingresses"
},
"ingress": {
"create_success": "Ingress created successfully",
"create_failed": "Failed to create ingress",
"client_required": "Client is required",
"server_required": "Server is required",
"select_client": "Select Client",
"select_server": "Select Server",
"creating": "Creating Ingress...",
"create_submit": "Create Ingress",
"title": "Ingress",
"create": "Create Ingress",
"create_title": "Create Ingress",
"create_description": "Create a new Ingress",
"no_ingress": "No Ingresses",
"edit": "Edit Ingress",
"delete": {
"title": "Delete Ingress",
"description": "Are you sure you want to delete this ingress?",
"button": "Delete"
},
"delete_success": "Ingress deleted successfully",
"delete_description": "Ingress deleted successfully"
},
"create": {
"button": "Create Worker",
"title": "Create Worker",
"description": "Create a new Worker",
"clientIdLabel": "Client ID",
"nameLabel": "Worker Name",
"namePlaceholder": "Enter Worker Name",
"creating": "Creating Worker...",
"submit": "Create Worker"
},
"columns": {
"name": "Name",
"status": "Status",
"id": "ID"
},
"actions_menu": {
"delete": "Delete Worker",
"title": "Worker Actions",
"edit": "Edit Worker"
},
"delete": {
"title": "Delete Worker",
"description": "Are you sure you want to delete this worker?",
"confirm": "Delete Worker"
},
"status": {
"no_resources": "No Resources",
"unusable": "Unusable",
"unhealthy": "Unhealthy",
"healthy": "Healthy",
"degraded": "Degraded",
"clients": "Deployments",
"running": "Running",
"ingresses": "Ingresses"
}
},
"status": {
"new": "New",
"wait_start": "Waiting to Start",
"start_error": "Start Error",
"running": "Running",
"check_failed": "Check Failed",
"error": "Error",
"unknown": "Unknown"
},
"nav": { "nav": {
"clients": "Clients", "clients": "Clients",
"servers": "Servers", "servers": "Servers",
@@ -478,7 +592,8 @@
"editServer": "Edit Server", "editServer": "Edit Server",
"trafficStats": "Traffic Stats", "trafficStats": "Traffic Stats",
"realTimeLog": "Real-time Log", "realTimeLog": "Real-time Log",
"console": "Console" "console": "Console",
"workers": "Functions"
}, },
"frpc_form": { "frpc_form": {
"add": "Add Client", "add": "Add Client",

View File

@@ -23,11 +23,8 @@
"streamlog": "Stream Log", "streamlog": "Stream Log",
"loading": "Loading...", "loading": "Loading...",
"error": "Error", "error": "Error",
"success": "Success",
"warning": "Warning", "warning": "Warning",
"info": "Information", "info": "Information",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete", "delete": "Delete",
"edit": "Edit", "edit": "Edit",
"newWindow": "New Window" "newWindow": "New Window"
@@ -121,5 +118,9 @@
"toggle": "Toggle Language", "toggle": "Toggle Language",
"zh": "Chinese", "zh": "Chinese",
"en": "English" "en": "English"
},
"worker": {
"name": "Worker Name",
"id": "Worker ID"
} }
} }

Some files were not shown because too many files have changed in this diff Show More