mirror of
https://github.com/VaalaCat/frp-panel.git
synced 2025-09-26 19:31:18 +08:00
feat: support cloudflare workerd
This commit is contained in:
48
.github/workflows/workerd-docker.workflow.yml
vendored
Normal file
48
.github/workflows/workerd-docker.workflow.yml
vendored
Normal 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
16
.ko.workerd.yaml
Normal 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
|
31
biz/client/create_worker.go
Normal file
31
biz/client/create_worker.go
Normal 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
|
||||
}
|
33
biz/client/get_worker_status.go
Normal file
33
biz/client/get_worker_status.go
Normal 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
|
||||
}
|
40
biz/client/install_workerd.go
Normal file
40
biz/client/install_workerd.go
Normal 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
|
||||
}
|
31
biz/client/remove_worker.go
Normal file
31
biz/client/remove_worker.go
Normal 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
|
||||
}
|
@@ -19,7 +19,7 @@ func HandleServerMessage(appInstance app.Application, req *pb.ServerMessage) *pb
|
||||
}
|
||||
}()
|
||||
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 {
|
||||
case pb.Event_EVENT_UPDATE_FRPC:
|
||||
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)
|
||||
case pb.Event_EVENT_GET_PROXY_INFO:
|
||||
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:
|
||||
rawData, _ := proto.Marshal(conf.GetVersion().ToProto())
|
||||
return &pb.ClientMessage{
|
||||
|
48
biz/client/rpc_pull_workers.go
Normal file
48
biz/client/rpc_pull_workers.go
Normal 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
|
||||
}
|
@@ -29,11 +29,13 @@ func StartFRPCHandler(ctx *app.Context, req *pb.StartFRPCRequest) (*pb.StartFRPC
|
||||
}, nil
|
||||
}
|
||||
|
||||
client, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID)
|
||||
cli, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := cli.ClientEntity
|
||||
|
||||
client.Stopped = false
|
||||
|
||||
if err = dao.NewQuery(ctx).UpdateClient(userInfo, client); err != nil {
|
||||
|
@@ -29,11 +29,13 @@ func StopFRPCHandler(ctx *app.Context, req *pb.StopFRPCRequest) (*pb.StopFRPCRes
|
||||
}, nil
|
||||
}
|
||||
|
||||
client, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID)
|
||||
cli, err := dao.NewQuery(ctx).GetClientByClientID(userInfo, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := cli.ClientEntity
|
||||
|
||||
client.Stopped = true
|
||||
|
||||
if err = dao.NewQuery(ctx).UpdateClient(userInfo, client); err != nil {
|
||||
|
@@ -36,7 +36,7 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
|
||||
}, err
|
||||
}
|
||||
|
||||
cli, err := dao.NewQuery(c).GetClientByClientID(userInfo, reqClientID)
|
||||
cliRecord, err := dao.NewQuery(c).GetClientByClientID(userInfo, reqClientID)
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", reqClientID)
|
||||
return &pb.UpdateFRPCResponse{
|
||||
@@ -44,6 +44,8 @@ func UpdateFrpcHander(c *app.Context, req *pb.UpdateFRPCRequest) (*pb.UpdateFRPC
|
||||
}, fmt.Errorf("cannot get client")
|
||||
}
|
||||
|
||||
cli := cliRecord.ClientEntity
|
||||
|
||||
if cli.IsShadow {
|
||||
cli, err = ChildClientForServer(c, serverID, cli)
|
||||
if err != nil {
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/VaalaCat/frp-panel/biz/master/shell"
|
||||
"github.com/VaalaCat/frp-panel/biz/master/streamlog"
|
||||
"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/services/app"
|
||||
"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("/delete", app.Wrapper(appInstance, client.DeleteClientHandler))
|
||||
clientRouter.POST("/list", app.Wrapper(appInstance, client.ListClientsHandler))
|
||||
clientRouter.POST("/install_workerd", app.Wrapper(appInstance, worker.InstallWorkerd))
|
||||
}
|
||||
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("/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("/log", streamlog.GetLogHandler(appInstance))
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ func CreateProxyConfig(c *app.Context, req *pb.CreateProxyConfigRequest) (*pb.Cr
|
||||
serverID = req.GetServerId()
|
||||
)
|
||||
|
||||
clientEntity, err := getClientWithMakeShadow(c, clientID, serverID)
|
||||
clientEntity, err := GetClientWithMakeShadow(c, clientID, serverID)
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
|
||||
return nil, err
|
||||
@@ -42,13 +42,6 @@ func CreateProxyConfig(c *app.Context, req *pb.CreateProxyConfigRequest) (*pb.Cr
|
||||
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())
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
if err := proxyCfg.FillTypedProxyConfig(typedProxyCfgs[0]); err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot fill typed proxy config")
|
||||
if err := CreateProxyConfigWithTypedConfig(c, CreateProxyConfigWithTypedConfigParam{
|
||||
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 &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
|
||||
existedProxyCfg, err = dao.NewQuery(c).GetProxyConfigByOriginClientIDAndName(userInfo, clientID, proxyCfg.Name)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
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)
|
||||
return nil, fmt.Errorf("proxy config already exist")
|
||||
return fmt.Errorf("proxy config already exist")
|
||||
}
|
||||
|
||||
// update client config
|
||||
if oldCfg, err := clientEntity.GetConfigContent(); err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot get client config, id: [%s]", clientID)
|
||||
return nil, err
|
||||
return err
|
||||
} else {
|
||||
oldCfg.Proxies = lo.Filter(oldCfg.Proxies, func(proxy v1.TypedProxyConfig, _ int) bool {
|
||||
return proxy.GetBaseConfig().Name != typedProxyCfgs[0].GetBaseConfig().Name
|
||||
return proxy.GetBaseConfig().Name != typedProxyCfg.GetBaseConfig().Name
|
||||
})
|
||||
oldCfg.Proxies = append(oldCfg.Proxies, typedProxyCfgs...)
|
||||
oldCfg.Proxies = append(oldCfg.Proxies, typedProxyCfg)
|
||||
|
||||
if err := clientEntity.SetConfigContent(*oldCfg); err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot set client config, id: [%s]", clientID)
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rawCfg, err := clientEntity.MarshalJSONConfig()
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot marshal client config, id: [%s]", clientID)
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.UpdateFrpcHander(c, &pb.UpdateFRPCRequest{
|
||||
@@ -117,11 +153,9 @@ func CreateProxyConfig(c *app.Context, req *pb.CreateProxyConfigRequest) (*pb.Cr
|
||||
Name: &proxyCfg.Name,
|
||||
}); err != nil {
|
||||
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{
|
||||
Status: &pb.Status{Code: pb.RespCode_RESP_CODE_SUCCESS, Message: "ok"},
|
||||
}, nil
|
||||
return nil
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ func DeleteProxyConfig(c *app.Context, req *pb.DeleteProxyConfigRequest) (*pb.De
|
||||
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)
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -29,11 +29,11 @@ func convertProxyStatsList(proxyList []*models.ProxyStatsEntity) []*pb.ProxyInfo
|
||||
})
|
||||
}
|
||||
|
||||
// getClientWithMakeShadow
|
||||
// GetClientWithMakeShadow
|
||||
// 1. 检查是否有已连接该服务端的客户端
|
||||
// 2. 检查是否有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)
|
||||
clientEntity, err := dao.NewQuery(c).GetClientByFilter(userInfo, &models.ClientEntity{OriginClientID: clientID, ServerID: serverID}, lo.ToPtr(false))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
@@ -21,7 +21,7 @@ func StartProxy(ctx *app.Context, req *pb.StartProxyRequest) (*pb.StartProxyResp
|
||||
proxyName = req.GetName()
|
||||
)
|
||||
|
||||
clientEntity, err := getClientWithMakeShadow(ctx, clientID, serverID)
|
||||
clientEntity, err := GetClientWithMakeShadow(ctx, clientID, serverID)
|
||||
if err != nil {
|
||||
logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
|
||||
return nil, err
|
||||
|
@@ -20,7 +20,7 @@ func StopProxy(ctx *app.Context, req *pb.StopProxyRequest) (*pb.StopProxyRespons
|
||||
proxyName = req.GetName()
|
||||
)
|
||||
|
||||
clientEntity, err := getClientWithMakeShadow(ctx, clientID, serverID)
|
||||
clientEntity, err := GetClientWithMakeShadow(ctx, clientID, serverID)
|
||||
if err != nil {
|
||||
logger.Logger(ctx).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
|
||||
return nil, err
|
||||
|
@@ -26,12 +26,14 @@ func UpdateProxyConfig(c *app.Context, req *pb.UpdateProxyConfigRequest) (*pb.Up
|
||||
serverID = req.GetServerId()
|
||||
)
|
||||
|
||||
clientEntity, err := dao.NewQuery(c).GetClientByClientID(userInfo, clientID)
|
||||
cli, err := dao.NewQuery(c).GetClientByClientID(userInfo, clientID)
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot get client, id: [%s]", clientID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientEntity := cli.ClientEntity
|
||||
|
||||
if clientEntity.ServerID != 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)
|
||||
@@ -40,7 +42,7 @@ func UpdateProxyConfig(c *app.Context, req *pb.UpdateProxyConfigRequest) (*pb.Up
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientEntity, err = client.ChildClientForServer(c, serverID, originClient)
|
||||
clientEntity, err = client.ChildClientForServer(c, serverID, originClient.ClientEntity)
|
||||
if err != nil {
|
||||
logger.Logger(c).WithError(err).Errorf("cannot create child client, id: [%s]", clientID)
|
||||
return nil, err
|
||||
|
72
biz/master/worker/create_worker.go
Normal file
72
biz/master/worker/create_worker.go
Normal 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
|
||||
}
|
106
biz/master/worker/create_worker_ingress.go
Normal file
106
biz/master/worker/create_worker_ingress.go
Normal 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
|
||||
}
|
46
biz/master/worker/get_worker.go
Normal file
46
biz/master/worker/get_worker.go
Normal 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
|
||||
}
|
32
biz/master/worker/get_worker_ingress.go
Normal file
32
biz/master/worker/get_worker_ingress.go
Normal 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
|
||||
}
|
65
biz/master/worker/get_worker_status.go
Normal file
65
biz/master/worker/get_worker_status.go
Normal 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
|
||||
}
|
38
biz/master/worker/install_workerd.go
Normal file
38
biz/master/worker/install_workerd.go
Normal 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
|
||||
}
|
100
biz/master/worker/list_worker.go
Normal file
100
biz/master/worker/list_worker.go
Normal 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
|
||||
}
|
75
biz/master/worker/remove_worker.go
Normal file
75
biz/master/worker/remove_worker.go
Normal 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
|
||||
}
|
1
biz/master/worker/run_worker.go
Normal file
1
biz/master/worker/run_worker.go
Normal file
@@ -0,0 +1 @@
|
||||
package worker
|
1
biz/master/worker/stop_worker.go
Normal file
1
biz/master/worker/stop_worker.go
Normal file
@@ -0,0 +1 @@
|
||||
package worker
|
104
biz/master/worker/update_worker.go
Normal file
104
biz/master/worker/update_worker.go
Normal 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
|
||||
}
|
@@ -21,10 +21,11 @@ type runClientParam struct {
|
||||
|
||||
Lc fx.Lifecycle
|
||||
|
||||
Ctx *app.Context
|
||||
AppInstance app.Application
|
||||
TaskManager watcher.Client `name:"clientTaskManager"`
|
||||
Cfg conf.Config
|
||||
Ctx *app.Context
|
||||
AppInstance app.Application
|
||||
TaskManager watcher.Client `name:"clientTaskManager"`
|
||||
WorkersManager app.WorkersManager
|
||||
Cfg conf.Config
|
||||
}
|
||||
|
||||
func runClient(param runClientParam) {
|
||||
@@ -45,6 +46,8 @@ func runClient(param runClientParam) {
|
||||
|
||||
param.TaskManager.AddDurationTask(defs.PullConfigDuration,
|
||||
bizclient.PullConfig, appInstance, clientID, clientSecret)
|
||||
param.TaskManager.AddDurationTask(defs.PullClientWorkersDuration,
|
||||
bizclient.PullWorkers, appInstance, clientID, clientSecret)
|
||||
|
||||
var wg conc.WaitGroup
|
||||
param.Lc.Append(fx.Hook{
|
||||
@@ -62,7 +65,10 @@ func runClient(param runClientParam) {
|
||||
)
|
||||
appInstance.SetClientRPCHandler(cliRpcHandler)
|
||||
|
||||
// --- init once start ---
|
||||
initClientOnce(appInstance, clientID, clientSecret)
|
||||
initClientWorkerOnce(appInstance, clientID, clientSecret)
|
||||
// --- init once stop ----
|
||||
|
||||
wg.Go(cliRpcHandler.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")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/VaalaCat/frp-panel/utils"
|
||||
"github.com/VaalaCat/frp-panel/utils/logger"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"go.uber.org/fx"
|
||||
@@ -383,13 +384,13 @@ func patchConfig(appInstance app.Application, commonArgs CommonArgs) conf.Config
|
||||
tmpCfg.Client.RPCUrl = *commonArgs.RpcUrl
|
||||
}
|
||||
|
||||
if commonArgs.RpcPort != nil || commonArgs.ApiPort != nil ||
|
||||
commonArgs.ApiScheme != nil ||
|
||||
commonArgs.RpcHost != nil || commonArgs.ApiHost != nil {
|
||||
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",
|
||||
if lo.FromPtrOr(commonArgs.RpcPort, 0) != 0 || lo.FromPtrOr(commonArgs.ApiPort, 0) != 0 ||
|
||||
lo.FromPtrOr(commonArgs.ApiScheme, "") != "" ||
|
||||
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, \n\n args: %s",
|
||||
tmpCfg.Master.RPCHost, tmpCfg.Master.RPCPort,
|
||||
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 {
|
||||
logger.Logger(c).Infof("env config, api url: %s, rpc url: %s", tmpCfg.Client.APIUrl, tmpCfg.Client.RPCUrl)
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ import (
|
||||
var (
|
||||
clientMod = fx.Module("cmd.client",
|
||||
fx.Provide(
|
||||
NewWorkerExecManager,
|
||||
NewWorkersManager,
|
||||
fx.Annotate(NewWatcher, fx.ResultTags(`name:"clientTaskManager"`)),
|
||||
))
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"embed"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"github.com/VaalaCat/frp-panel/services/rbac"
|
||||
"github.com/VaalaCat/frp-panel/services/rpc"
|
||||
"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/logger"
|
||||
"github.com/VaalaCat/frp-panel/utils/wsgrpc"
|
||||
@@ -373,3 +375,39 @@ func NewEnforcer(param struct {
|
||||
param.AppInstance.SetEnforcer(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
|
||||
}
|
||||
|
@@ -25,7 +25,10 @@ type ReqType interface {
|
||||
pb.GetProxyStatsByClientIDRequest | pb.GetProxyStatsByServerIDRequest |
|
||||
pb.CreateProxyConfigRequest | pb.ListProxyConfigsRequest | pb.UpdateProxyConfigRequest |
|
||||
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) {
|
||||
|
@@ -26,7 +26,10 @@ type RespType interface {
|
||||
pb.GetProxyStatsByClientIDResponse | pb.GetProxyStatsByServerIDResponse |
|
||||
pb.CreateProxyConfigResponse | pb.ListProxyConfigsResponse | pb.UpdateProxyConfigResponse |
|
||||
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) {
|
||||
@@ -96,6 +99,14 @@ func getEvent(origin interface{}) (pb.Event, protoreflect.ProtoMessage, error) {
|
||||
return pb.Event_EVENT_STOP_FRPS, ptr, nil
|
||||
case *pb.GetProxyConfigResponse:
|
||||
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:
|
||||
return 0, nil, fmt.Errorf("cannot unmarshal unknown type: %T", origin)
|
||||
}
|
||||
|
@@ -5,8 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/VaalaCat/frp-panel/defs"
|
||||
"github.com/VaalaCat/frp-panel/utils"
|
||||
"github.com/VaalaCat/frp-panel/utils/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"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"`
|
||||
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"`
|
||||
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_"`
|
||||
IsDebug bool `env:"IS_DEBUG" env-default:"false" env-description:"is debug mode"`
|
||||
Logger struct {
|
||||
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"`
|
||||
} `env-prefix:"LOGGER_"`
|
||||
HTTP_PROXY string `env:"HTTP_PROXY" env-description:"http proxy"`
|
||||
}
|
||||
|
||||
func NewConfig() Config {
|
||||
@@ -115,6 +130,21 @@ func (cfg *Config) Complete() {
|
||||
if len(cfg.Client.ID) == 0 {
|
||||
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 {
|
||||
|
@@ -68,8 +68,9 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
PullConfigDuration = 30 * time.Second
|
||||
PushProxyInfoDuration = 30 * time.Second
|
||||
PullConfigDuration = 30 * time.Second
|
||||
PushProxyInfoDuration = 30 * time.Second
|
||||
PullClientWorkersDuration = 30 * time.Second
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -100,6 +101,50 @@ const (
|
||||
const (
|
||||
UserRole_Admin = "admin"
|
||||
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
|
||||
@@ -109,3 +154,23 @@ const (
|
||||
TokenStatusInactive TokenStatus = "inactive"
|
||||
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
15
go.mod
@@ -7,7 +7,7 @@ toolchain go1.24.1
|
||||
require (
|
||||
github.com/UserExistsError/conpty v0.1.4
|
||||
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/creack/pty v1.1.24
|
||||
github.com/fatedier/frp v0.62.0
|
||||
@@ -25,23 +25,26 @@ require (
|
||||
github.com/jackpal/gateway v1.0.16
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kardianos/service v1.2.2
|
||||
github.com/lucasepe/codename v0.2.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/sourcegraph/conc v0.3.0
|
||||
github.com/spf13/cast v1.7.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/pretty v1.2.1
|
||||
github.com/tiendc/go-deepcopy v1.2.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/net v0.39.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gorm.io/driver/mysql v1.5.7
|
||||
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
|
||||
)
|
||||
|
||||
@@ -60,7 +63,7 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/gin-contrib/sse v0.1.0 // 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/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // 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/xorsimd v0.4.3 // 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/dig v1.18.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
|
||||
golang.org/x/arch v0.3.0 // 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.v3 v3.0.1 // 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
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
|
28
go.sum
28
go.sum
@@ -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/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I=
|
||||
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.32.0/go.mod h1:Zre/H8p17mpv5U3EaWgPoxLILLdXO3gHW5aoQQpUDZI=
|
||||
github.com/casbin/gorm-adapter/v3 v3.29.0 h1:MpbF5JVFYOwVGaTkvwDNUV4k+5CYwAu6v83ofZHRfvM=
|
||||
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/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||
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/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/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
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/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
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/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
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/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
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.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
|
||||
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
|
||||
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/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
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/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||
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.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
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/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
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/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.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/plugin/dbresolver v1.5.3 h1:wFwINGZZmttuu9h7XpvbDHd8Lf9bb8GNzp/NpAMV2wU=
|
||||
gorm.io/plugin/dbresolver v1.5.3/go.mod h1:TSrVhaUg2DZAWP3PrHlDlITEJmNOkL0tFTjvTEsQ4XE=
|
||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gorm.io/plugin/dbresolver v1.5.2 h1:Iut7lW4TXNoVs++I+ra3zxjSxTRj4ocIeFEVp4lLhII=
|
||||
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/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@@ -167,3 +167,111 @@ message StartProxyRequest {
|
||||
message StartProxyResponse {
|
||||
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;
|
||||
}
|
||||
|
@@ -97,3 +97,25 @@ message ProxyWorkingStatus {
|
||||
optional string err = 4;
|
||||
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;
|
||||
}
|
||||
|
@@ -24,6 +24,10 @@ enum Event {
|
||||
EVENT_STOP_STREAM_LOG = 16;
|
||||
EVENT_START_PTY_CONNECT = 17;
|
||||
EVENT_GET_PROXY_INFO = 18;
|
||||
EVENT_CREATE_WORKER = 19;
|
||||
EVENT_REMOVE_WORKER = 20;
|
||||
EVENT_GET_WORKER_STATUS = 21;
|
||||
EVENT_INSTALL_WORKERD = 22;
|
||||
}
|
||||
|
||||
message ServerBase {
|
||||
@@ -122,10 +126,20 @@ message PTYServerMessage {
|
||||
bool done = 4;
|
||||
}
|
||||
|
||||
message ListClientWorkersRequest {
|
||||
ClientBase base = 255;
|
||||
}
|
||||
|
||||
message ListClientWorkersResponse {
|
||||
common.Status status = 1;
|
||||
repeated common.Worker workers = 2;
|
||||
}
|
||||
|
||||
service Master {
|
||||
rpc ServerSend(stream ClientMessage) returns(stream ServerMessage);
|
||||
rpc PullClientConfig(PullClientConfigReq) returns(PullClientConfigResp);
|
||||
rpc PullServerConfig(PullServerConfigReq) returns(PullServerConfigResp);
|
||||
rpc ListClientWorkers(ListClientWorkersRequest) returns(ListClientWorkersResponse);
|
||||
rpc FRPCAuth(FRPAuthRequest) returns(FRPAuthResponse);
|
||||
rpc PushProxyInfo(PushProxyInfoReq) returns(PushProxyInfoResp);
|
||||
rpc PushClientStreamLog(stream PushClientStreamLogReq) returns(PushStreamLogResp);
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/VaalaCat/frp-panel/pb"
|
||||
"github.com/VaalaCat/frp-panel/utils"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/samber/lo"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
|
||||
type Client struct {
|
||||
*ClientEntity
|
||||
Workers []*Worker `json:"workers,omitempty" gorm:"many2many:worker_clients;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||
}
|
||||
|
||||
type ClientEntity struct {
|
||||
@@ -75,3 +77,22 @@ func (c *ClientEntity) MarshalJSONConfig() ([]byte, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@@ -35,6 +35,9 @@ func (dbm *dbManagerImpl) Init() {
|
||||
if err := db.AutoMigrate(&HistoryProxyStats{}); err != nil {
|
||||
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 {
|
||||
logger.Logger(context.Background()).WithError(err).Fatalf("cannot init db table [%s]", (&ProxyConfig{}).TableName())
|
||||
}
|
||||
|
@@ -3,13 +3,19 @@ package models
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/VaalaCat/frp-panel/defs"
|
||||
"github.com/VaalaCat/frp-panel/pb"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProxyConfig struct {
|
||||
*gorm.Model
|
||||
*ProxyConfigEntity
|
||||
|
||||
WorkerID string `gorm:"type:varchar(255);index"` // 引用的worker
|
||||
Worker Worker
|
||||
}
|
||||
|
||||
type ProxyConfigEntity struct {
|
||||
@@ -53,3 +59,36 @@ func (p *ProxyConfigEntity) GetTypedProxyConfig() (v1.TypedProxyConfig, error) {
|
||||
err := cfg.UnmarshalJSON(p.Content)
|
||||
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
86
models/worker.go
Normal 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
|
||||
}
|
1375
pb/api_client.pb.go
1375
pb/api_client.pb.go
File diff suppressed because it is too large
Load Diff
265
pb/common.pb.go
265
pb/common.pb.go
@@ -871,6 +871,211 @@ func (x *ProxyWorkingStatus) GetRemoteAddr() string {
|
||||
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
|
||||
|
||||
const file_common_proto_rawDesc = "" +
|
||||
@@ -999,7 +1204,39 @@ const file_common_proto_rawDesc = "" +
|
||||
"\x05_typeB\t\n" +
|
||||
"\a_statusB\x06\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" +
|
||||
"\x15RESP_CODE_UNSPECIFIED\x10\x00\x12\x15\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_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_common_proto_goTypes = []any{
|
||||
(RespCode)(0), // 0: common.RespCode
|
||||
(ClientType)(0), // 1: common.ClientType
|
||||
@@ -1040,15 +1277,20 @@ var file_common_proto_goTypes = []any{
|
||||
(*ProxyInfo)(nil), // 8: common.ProxyInfo
|
||||
(*ProxyConfig)(nil), // 9: common.ProxyConfig
|
||||
(*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{
|
||||
0, // 0: common.Status.code:type_name -> common.RespCode
|
||||
2, // 1: common.CommonResponse.status:type_name -> common.Status
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
0, // 0: common.Status.code:type_name -> common.RespCode
|
||||
2, // 1: common.CommonResponse.status:type_name -> common.Status
|
||||
13, // 2: common.Worker.socket:type_name -> common.Socket
|
||||
11, // 3: common.WorkerList.workers:type_name -> common.Worker
|
||||
4, // [4:4] is the sub-list for method output_type
|
||||
4, // [4:4] is the sub-list for method input_type
|
||||
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() }
|
||||
@@ -1064,13 +1306,16 @@ func file_common_proto_init() {
|
||||
file_common_proto_msgTypes[6].OneofWrappers = []any{}
|
||||
file_common_proto_msgTypes[7].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{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)),
|
||||
NumEnums: 2,
|
||||
NumMessages: 9,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
@@ -43,6 +43,10 @@ const (
|
||||
Event_EVENT_STOP_STREAM_LOG Event = 16
|
||||
Event_EVENT_START_PTY_CONNECT Event = 17
|
||||
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.
|
||||
@@ -67,6 +71,10 @@ var (
|
||||
16: "EVENT_STOP_STREAM_LOG",
|
||||
17: "EVENT_START_PTY_CONNECT",
|
||||
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_UNSPECIFIED": 0,
|
||||
@@ -88,6 +96,10 @@ var (
|
||||
"EVENT_STOP_STREAM_LOG": 16,
|
||||
"EVENT_START_PTY_CONNECT": 17,
|
||||
"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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const file_rpc_master_proto_rawDesc = "" +
|
||||
@@ -1172,7 +1280,12 @@ const file_rpc_master_proto_rawDesc = "" +
|
||||
"\x04done\x18\x04 \x01(\bR\x04doneB\a\n" +
|
||||
"\x05_dataB\t\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" +
|
||||
"\x11EVENT_UNSPECIFIED\x10\x00\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" +
|
||||
"\x15EVENT_STOP_STREAM_LOG\x10\x10\x12\x1b\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" +
|
||||
"\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" +
|
||||
"\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" +
|
||||
"\rPushProxyInfo\x12\x18.master.PushProxyInfoReq\x1a\x19.master.PushProxyInfoResp\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_msgTypes = make([]protoimpl.MessageInfo, 17)
|
||||
var file_rpc_master_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
|
||||
var file_rpc_master_proto_goTypes = []any{
|
||||
(Event)(0), // 0: master.Event
|
||||
(*ServerBase)(nil), // 1: master.ServerBase
|
||||
(*ClientBase)(nil), // 2: master.ClientBase
|
||||
(*ServerMessage)(nil), // 3: master.ServerMessage
|
||||
(*ClientMessage)(nil), // 4: master.ClientMessage
|
||||
(*PullClientConfigReq)(nil), // 5: master.PullClientConfigReq
|
||||
(*PullClientConfigResp)(nil), // 6: master.PullClientConfigResp
|
||||
(*PullServerConfigReq)(nil), // 7: master.PullServerConfigReq
|
||||
(*PullServerConfigResp)(nil), // 8: master.PullServerConfigResp
|
||||
(*FRPAuthRequest)(nil), // 9: master.FRPAuthRequest
|
||||
(*FRPAuthResponse)(nil), // 10: master.FRPAuthResponse
|
||||
(*PushProxyInfoReq)(nil), // 11: master.PushProxyInfoReq
|
||||
(*PushProxyInfoResp)(nil), // 12: master.PushProxyInfoResp
|
||||
(*PushServerStreamLogReq)(nil), // 13: master.PushServerStreamLogReq
|
||||
(*PushClientStreamLogReq)(nil), // 14: master.PushClientStreamLogReq
|
||||
(*PushStreamLogResp)(nil), // 15: master.PushStreamLogResp
|
||||
(*PTYClientMessage)(nil), // 16: master.PTYClientMessage
|
||||
(*PTYServerMessage)(nil), // 17: master.PTYServerMessage
|
||||
(*Status)(nil), // 18: common.Status
|
||||
(*Client)(nil), // 19: common.Client
|
||||
(*Server)(nil), // 20: common.Server
|
||||
(*ProxyInfo)(nil), // 21: common.ProxyInfo
|
||||
(Event)(0), // 0: master.Event
|
||||
(*ServerBase)(nil), // 1: master.ServerBase
|
||||
(*ClientBase)(nil), // 2: master.ClientBase
|
||||
(*ServerMessage)(nil), // 3: master.ServerMessage
|
||||
(*ClientMessage)(nil), // 4: master.ClientMessage
|
||||
(*PullClientConfigReq)(nil), // 5: master.PullClientConfigReq
|
||||
(*PullClientConfigResp)(nil), // 6: master.PullClientConfigResp
|
||||
(*PullServerConfigReq)(nil), // 7: master.PullServerConfigReq
|
||||
(*PullServerConfigResp)(nil), // 8: master.PullServerConfigResp
|
||||
(*FRPAuthRequest)(nil), // 9: master.FRPAuthRequest
|
||||
(*FRPAuthResponse)(nil), // 10: master.FRPAuthResponse
|
||||
(*PushProxyInfoReq)(nil), // 11: master.PushProxyInfoReq
|
||||
(*PushProxyInfoResp)(nil), // 12: master.PushProxyInfoResp
|
||||
(*PushServerStreamLogReq)(nil), // 13: master.PushServerStreamLogReq
|
||||
(*PushClientStreamLogReq)(nil), // 14: master.PushClientStreamLogReq
|
||||
(*PushStreamLogResp)(nil), // 15: master.PushStreamLogResp
|
||||
(*PTYClientMessage)(nil), // 16: master.PTYClientMessage
|
||||
(*PTYServerMessage)(nil), // 17: master.PTYServerMessage
|
||||
(*ListClientWorkersRequest)(nil), // 18: master.ListClientWorkersRequest
|
||||
(*ListClientWorkersResponse)(nil), // 19: master.ListClientWorkersResponse
|
||||
(*Status)(nil), // 20: common.Status
|
||||
(*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{
|
||||
0, // 0: master.ServerMessage.event:type_name -> master.Event
|
||||
0, // 1: master.ClientMessage.event:type_name -> master.Event
|
||||
2, // 2: master.PullClientConfigReq.base:type_name -> master.ClientBase
|
||||
18, // 3: master.PullClientConfigResp.status:type_name -> common.Status
|
||||
19, // 4: master.PullClientConfigResp.client:type_name -> common.Client
|
||||
20, // 3: master.PullClientConfigResp.status:type_name -> common.Status
|
||||
21, // 4: master.PullClientConfigResp.client:type_name -> common.Client
|
||||
1, // 5: master.PullServerConfigReq.base:type_name -> master.ServerBase
|
||||
18, // 6: master.PullServerConfigResp.status:type_name -> common.Status
|
||||
20, // 7: master.PullServerConfigResp.server:type_name -> common.Server
|
||||
20, // 6: master.PullServerConfigResp.status:type_name -> common.Status
|
||||
22, // 7: master.PullServerConfigResp.server:type_name -> common.Server
|
||||
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
|
||||
21, // 11: master.PushProxyInfoReq.proxy_infos:type_name -> common.ProxyInfo
|
||||
18, // 12: master.PushProxyInfoResp.status:type_name -> common.Status
|
||||
23, // 11: master.PushProxyInfoReq.proxy_infos:type_name -> common.ProxyInfo
|
||||
20, // 12: master.PushProxyInfoResp.status:type_name -> common.Status
|
||||
1, // 13: master.PushServerStreamLogReq.base:type_name -> master.ServerBase
|
||||
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
|
||||
2, // 17: master.PTYClientMessage.client_base:type_name -> master.ClientBase
|
||||
4, // 18: master.Master.ServerSend:input_type -> master.ClientMessage
|
||||
5, // 19: master.Master.PullClientConfig:input_type -> master.PullClientConfigReq
|
||||
7, // 20: master.Master.PullServerConfig:input_type -> master.PullServerConfigReq
|
||||
9, // 21: master.Master.FRPCAuth:input_type -> master.FRPAuthRequest
|
||||
11, // 22: master.Master.PushProxyInfo:input_type -> master.PushProxyInfoReq
|
||||
14, // 23: master.Master.PushClientStreamLog:input_type -> master.PushClientStreamLogReq
|
||||
13, // 24: master.Master.PushServerStreamLog:input_type -> master.PushServerStreamLogReq
|
||||
16, // 25: master.Master.PTYConnect:input_type -> master.PTYClientMessage
|
||||
3, // 26: master.Master.ServerSend:output_type -> master.ServerMessage
|
||||
6, // 27: master.Master.PullClientConfig:output_type -> master.PullClientConfigResp
|
||||
8, // 28: master.Master.PullServerConfig:output_type -> master.PullServerConfigResp
|
||||
10, // 29: master.Master.FRPCAuth:output_type -> master.FRPAuthResponse
|
||||
12, // 30: master.Master.PushProxyInfo:output_type -> master.PushProxyInfoResp
|
||||
15, // 31: master.Master.PushClientStreamLog:output_type -> master.PushStreamLogResp
|
||||
15, // 32: master.Master.PushServerStreamLog:output_type -> master.PushStreamLogResp
|
||||
17, // 33: master.Master.PTYConnect:output_type -> master.PTYServerMessage
|
||||
26, // [26:34] is the sub-list for method output_type
|
||||
18, // [18:26] is the sub-list for method input_type
|
||||
18, // [18:18] is the sub-list for extension type_name
|
||||
18, // [18:18] is the sub-list for extension extendee
|
||||
0, // [0:18] is the sub-list for field type_name
|
||||
2, // 18: master.ListClientWorkersRequest.base:type_name -> master.ClientBase
|
||||
20, // 19: master.ListClientWorkersResponse.status:type_name -> common.Status
|
||||
24, // 20: master.ListClientWorkersResponse.workers:type_name -> common.Worker
|
||||
4, // 21: master.Master.ServerSend:input_type -> master.ClientMessage
|
||||
5, // 22: master.Master.PullClientConfig:input_type -> master.PullClientConfigReq
|
||||
7, // 23: master.Master.PullServerConfig:input_type -> master.PullServerConfigReq
|
||||
18, // 24: master.Master.ListClientWorkers:input_type -> master.ListClientWorkersRequest
|
||||
9, // 25: master.Master.FRPCAuth:input_type -> master.FRPAuthRequest
|
||||
11, // 26: master.Master.PushProxyInfo:input_type -> master.PushProxyInfoReq
|
||||
14, // 27: master.Master.PushClientStreamLog:input_type -> master.PushClientStreamLogReq
|
||||
13, // 28: master.Master.PushServerStreamLog:input_type -> master.PushServerStreamLogReq
|
||||
16, // 29: master.Master.PTYConnect:input_type -> master.PTYClientMessage
|
||||
3, // 30: master.Master.ServerSend:output_type -> master.ServerMessage
|
||||
6, // 31: master.Master.PullClientConfig:output_type -> master.PullClientConfigResp
|
||||
8, // 32: master.Master.PullServerConfig:output_type -> master.PullServerConfigResp
|
||||
19, // 33: master.Master.ListClientWorkers:output_type -> master.ListClientWorkersResponse
|
||||
10, // 34: master.Master.FRPCAuth:output_type -> master.FRPAuthResponse
|
||||
12, // 35: master.Master.PushProxyInfo:output_type -> master.PushProxyInfoResp
|
||||
15, // 36: master.Master.PushClientStreamLog:output_type -> master.PushStreamLogResp
|
||||
15, // 37: master.Master.PushServerStreamLog:output_type -> master.PushStreamLogResp
|
||||
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() }
|
||||
@@ -1306,7 +1432,7 @@ func file_rpc_master_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_master_proto_rawDesc), len(file_rpc_master_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 17,
|
||||
NumMessages: 19,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
@@ -22,6 +22,7 @@ const (
|
||||
Master_ServerSend_FullMethodName = "/master.Master/ServerSend"
|
||||
Master_PullClientConfig_FullMethodName = "/master.Master/PullClientConfig"
|
||||
Master_PullServerConfig_FullMethodName = "/master.Master/PullServerConfig"
|
||||
Master_ListClientWorkers_FullMethodName = "/master.Master/ListClientWorkers"
|
||||
Master_FRPCAuth_FullMethodName = "/master.Master/FRPCAuth"
|
||||
Master_PushProxyInfo_FullMethodName = "/master.Master/PushProxyInfo"
|
||||
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)
|
||||
PullClientConfig(ctx context.Context, in *PullClientConfigReq, opts ...grpc.CallOption) (*PullClientConfigResp, 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)
|
||||
PushProxyInfo(ctx context.Context, in *PushProxyInfoReq, opts ...grpc.CallOption) (*PushProxyInfoResp, 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
|
||||
}
|
||||
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(FRPAuthResponse)
|
||||
@@ -150,6 +162,7 @@ type MasterServer interface {
|
||||
ServerSend(grpc.BidiStreamingServer[ClientMessage, ServerMessage]) error
|
||||
PullClientConfig(context.Context, *PullClientConfigReq) (*PullClientConfigResp, error)
|
||||
PullServerConfig(context.Context, *PullServerConfigReq) (*PullServerConfigResp, error)
|
||||
ListClientWorkers(context.Context, *ListClientWorkersRequest) (*ListClientWorkersResponse, error)
|
||||
FRPCAuth(context.Context, *FRPAuthRequest) (*FRPAuthResponse, error)
|
||||
PushProxyInfo(context.Context, *PushProxyInfoReq) (*PushProxyInfoResp, 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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
in := new(FRPAuthRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -325,6 +359,10 @@ var Master_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "PullServerConfig",
|
||||
Handler: _Master_PullServerConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListClientWorkers",
|
||||
Handler: _Master_ListClientWorkers_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "FRPCAuth",
|
||||
Handler: _Master_FRPCAuth_Handler,
|
||||
|
@@ -12,20 +12,42 @@ type application struct {
|
||||
streamLogHookMgr StreamLogHookMgr
|
||||
masterCli MasterClient
|
||||
|
||||
shellPTYMgr ShellPTYMgr
|
||||
clientLogManager ClientLogManager
|
||||
clientRPCHandler ClientRPCHandler
|
||||
dbManager DBManager
|
||||
clientController ClientController
|
||||
clientRecvMap *sync.Map
|
||||
clientsManager ClientsManager
|
||||
serverHandler ServerHandler
|
||||
serverController ServerController
|
||||
rpcCred credentials.TransportCredentials
|
||||
conf conf.Config
|
||||
currentRole string
|
||||
permManager PermissionManager
|
||||
enforcer *casbin.Enforcer
|
||||
shellPTYMgr ShellPTYMgr
|
||||
clientLogManager ClientLogManager
|
||||
clientRPCHandler ClientRPCHandler
|
||||
dbManager DBManager
|
||||
clientController ClientController
|
||||
clientRecvMap *sync.Map
|
||||
clientsManager ClientsManager
|
||||
serverHandler ServerHandler
|
||||
serverController ServerController
|
||||
rpcCred credentials.TransportCredentials
|
||||
conf conf.Config
|
||||
currentRole string
|
||||
permManager PermissionManager
|
||||
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.
|
||||
|
@@ -43,6 +43,10 @@ type Application interface {
|
||||
SetEnforcer(*casbin.Enforcer)
|
||||
GetPermManager() PermissionManager
|
||||
SetPermManager(PermissionManager)
|
||||
GetWorkerExecManager() WorkerExecManager
|
||||
SetWorkerExecManager(WorkerExecManager)
|
||||
GetWorkersManager() WorkersManager
|
||||
SetWorkersManager(WorkersManager)
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
|
@@ -61,7 +61,7 @@ func WrapperServerMsg[T common.ReqType, U common.RespType](appInstance Applicati
|
||||
|
||||
cliMsg, err := common.ProtoResp(resp)
|
||||
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{
|
||||
Event: pb.Event_EVENT_ERROR,
|
||||
Data: []byte(err.Error()),
|
||||
|
@@ -171,3 +171,30 @@ type PermissionManager interface {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ func (q *queryImpl) ValidateClientSecret(clientID, clientSecret string) (*models
|
||||
return c.ClientEntity, nil
|
||||
}
|
||||
|
||||
func (q *queryImpl) AdminGetClientByClientID(clientID string) (*models.ClientEntity, error) {
|
||||
func (q *queryImpl) AdminGetClientByClientID(clientID string) (*models.Client, error) {
|
||||
if clientID == "" {
|
||||
return nil, fmt.Errorf("invalid client id")
|
||||
}
|
||||
@@ -43,10 +43,10 @@ func (q *queryImpl) AdminGetClientByClientID(clientID string) (*models.ClientEnt
|
||||
if err != nil {
|
||||
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 == "" {
|
||||
return nil, fmt.Errorf("invalid client id")
|
||||
}
|
||||
@@ -62,7 +62,22 @@ func (q *queryImpl) GetClientByClientID(userInfo models.UserInfo, clientID strin
|
||||
if err != nil {
|
||||
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) {
|
||||
|
@@ -548,3 +548,21 @@ func (q *queryImpl) CountProxyConfigsWithFiltersAndKeyword(userInfo models.UserI
|
||||
}
|
||||
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
166
services/dao/worker.go
Normal 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
|
||||
}
|
@@ -10,6 +10,7 @@ import (
|
||||
masterserver "github.com/VaalaCat/frp-panel/biz/master/server"
|
||||
"github.com/VaalaCat/frp-panel/biz/master/shell"
|
||||
"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/defs"
|
||||
"github.com/VaalaCat/frp-panel/pb"
|
||||
@@ -26,6 +27,21 @@ type server struct {
|
||||
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 {
|
||||
// s := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
|
||||
// s := grpc.NewServer(grpc.Creds(creds))
|
||||
|
48
services/port/manager.go
Normal file
48
services/port/manager.go
Normal 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
|
||||
}
|
117
services/workerd/exec_manager.go
Normal file
117
services/workerd/exec_manager.go
Normal 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
|
||||
}
|
40
services/workerd/exec_manager_windows.go
Normal file
40
services/workerd/exec_manager_windows.go
Normal 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
44
services/workerd/file.go
Normal 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,
|
||||
)
|
||||
}
|
49
services/workerd/helper.go
Normal file
49
services/workerd/helper.go
Normal 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)
|
||||
}
|
93
services/workerd/workerd.go
Normal file
93
services/workerd/workerd.go
Normal 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)
|
||||
}
|
||||
}
|
67
services/workerd/workerd_conf_gen.go
Normal file
67
services/workerd/workerd_conf_gen.go
Normal 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)
|
||||
}
|
37
services/workerd/workerd_conf_gen_all.go
Normal file
37
services/workerd/workerd_conf_gen_all.go
Normal 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
|
||||
}
|
93
services/workerd/workerd_conf_gen_test.go
Normal file
93
services/workerd/workerd_conf_gen_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
43
services/workerd/workerd_test.go
Normal file
43
services/workerd/workerd_test.go
Normal 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()
|
||||
}
|
126
services/workerd/workers_manager.go
Normal file
126
services/workerd/workers_manager.go
Normal 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
24
utils/addr.go
Normal 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
8
utils/codename.go
Normal 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
134
utils/file.go
Normal 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()`
|
||||
}
|
178
utils/files.go
178
utils/files.go
@@ -1,8 +1,18 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/VaalaCat/frp-panel/utils/logger"
|
||||
"github.com/imroc/req/v3"
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
func EnsureDirectoryExists(filePath string) error {
|
||||
@@ -16,3 +26,171 @@ func EnsureDirectoryExists(filePath string) error {
|
||||
}
|
||||
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 non‐executable
|
||||
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 file‐specific 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
52
utils/port.go
Normal 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
32
utils/process.go
Normal 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
|
||||
}
|
@@ -180,3 +180,16 @@ func (s *SyncMap[K, V]) Values() []V {
|
||||
|
||||
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
15
utils/uuid.go
Normal 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
69
www/api/worker.ts
Normal 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)
|
||||
}
|
@@ -1,24 +1,24 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query'
|
||||
import { listClient } from '@/api/client'
|
||||
import { Combobox } from './combobox'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Client } from '@/lib/pb/common'
|
||||
|
||||
export interface ClientSelectorProps {
|
||||
clientID?: string
|
||||
setClientID: (clientID: string) => void
|
||||
clients?: Client[]
|
||||
onOpenChange?: () => void
|
||||
}
|
||||
|
||||
export const ClientSelector: React.FC<ClientSelectorProps> = ({
|
||||
clientID,
|
||||
setClientID,
|
||||
onOpenChange
|
||||
}) => {
|
||||
export const ClientSelector: React.FC<ClientSelectorProps> = ({ clientID, setClientID, clients, onOpenChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const handleClientChange = (value: string) => { setClientID(value) }
|
||||
const handleClientChange = (value: string) => {
|
||||
setClientID(value)
|
||||
}
|
||||
const [keyword, setKeyword] = React.useState('')
|
||||
|
||||
const { data: clientList, refetch: refetchClients } = useQuery({
|
||||
@@ -27,15 +27,23 @@ export const ClientSelector: React.FC<ClientSelectorProps> = ({
|
||||
return listClient({ page: 1, pageSize: 8, keyword: keyword })
|
||||
},
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: clients === undefined,
|
||||
})
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
placeholder={t('selector.client.placeholder')}
|
||||
dataList={clientList?.clients.map((client) => ({
|
||||
value: client.id || '',
|
||||
label: client.id || ''
|
||||
})) || []}
|
||||
dataList={
|
||||
clients !== undefined
|
||||
? clients.map((client) => ({
|
||||
value: client.id || '',
|
||||
label: client.id || '',
|
||||
}))
|
||||
: clientList?.clients.map((client) => ({
|
||||
value: client.id || '',
|
||||
label: client.id || '',
|
||||
})) || []
|
||||
}
|
||||
setValue={handleClientChange}
|
||||
value={clientID}
|
||||
onKeyWordChange={setKeyword}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { Popover, PopoverTrigger } from "@radix-ui/react-popover"
|
||||
import { Badge } from "../ui/badge"
|
||||
import { ClientStatus } from "@/lib/pb/api_master"
|
||||
import { PopoverContent } from "../ui/popover"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { motion } from "framer-motion"
|
||||
import { Popover, PopoverTrigger } from '@radix-ui/react-popover'
|
||||
import { Badge } from '../ui/badge'
|
||||
import { ClientStatus } from '@/lib/pb/api_master'
|
||||
import { PopoverContent } from '../ui/popover'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { motion } from 'framer-motion'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { zhCN, enUS } from 'date-fns/locale'
|
||||
|
||||
@@ -13,31 +13,26 @@ export const ClientDetail = ({ clientStatus }: { clientStatus: ClientStatus }) =
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const locale = i18n.language === 'zh' ? zhCN : enUS
|
||||
const connectTime = clientStatus.connectTime ?
|
||||
formatDistanceToNow(new Date(parseInt(clientStatus.connectTime.toString())), {
|
||||
addSuffix: true,
|
||||
locale
|
||||
}) : '-'
|
||||
const connectTime = clientStatus.connectTime
|
||||
? formatDistanceToNow(new Date(parseInt(clientStatus.connectTime.toString())), {
|
||||
addSuffix: true,
|
||||
locale,
|
||||
})
|
||||
: '-'
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger className='flex items-center'>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className='text-nowrap rounded-full h-6 hover:bg-secondary/80 transition-colors text-sm'
|
||||
<PopoverTrigger className="flex items-center">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-nowrap rounded-full h-6 hover:bg-secondary/80 transition-colors text-xs"
|
||||
>
|
||||
{clientStatus.version?.gitVersion || 'Unknown'}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-72 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-border">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
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>
|
||||
<motion.div initial={{ opacity: 0, y: -10 }} 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="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>
|
||||
@@ -68,4 +63,4 @@ export const ClientDetail = ({ clientStatus }: { clientStatus: ClientStatus }) =
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
26
www/components/base/monaco.tsx
Normal file
26
www/components/base/monaco.tsx
Normal 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>
|
||||
)
|
||||
}
|
@@ -146,7 +146,7 @@ export const TCPProxyForm: React.FC<ProxyFormProps> = ({
|
||||
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
|
||||
|
||||
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)) {
|
||||
toast.error('Invalid configuration')
|
||||
return
|
||||
@@ -252,7 +252,7 @@ export const STCPProxyForm: React.FC<ProxyFormProps> = ({
|
||||
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
|
||||
|
||||
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)) {
|
||||
toast.error('Invalid configuration')
|
||||
return
|
||||
@@ -334,7 +334,7 @@ export const UDPProxyForm: React.FC<ProxyFormProps> = ({
|
||||
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
|
||||
|
||||
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)) {
|
||||
toast.error('Invalid configuration')
|
||||
return
|
||||
@@ -442,7 +442,7 @@ export const HTTPProxyForm: React.FC<ProxyFormProps> = ({
|
||||
const [pluginConfig, setPluginConfig] = useState<TypedClientPluginOptions | undefined>(defaultConfig.plugin)
|
||||
|
||||
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)) {
|
||||
toast.error('Invalid configuration')
|
||||
return
|
||||
|
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
@@ -33,6 +33,7 @@ export type ProxyConfigMutateDialogProps = {
|
||||
defaultProxyConfig?: TypedProxyConfig
|
||||
defaultOriginalProxyConfig?: ProxyConfig
|
||||
disableChangeProxyName?: boolean
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const ProxyConfigMutateDialog = ({ ...props }: ProxyConfigMutateDialogProps) => {
|
||||
@@ -41,16 +42,14 @@ export const ProxyConfigMutateDialog = ({ ...props }: ProxyConfigMutateDialogPro
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className='w-fit'>
|
||||
<Button variant="outline" className="w-fit">
|
||||
{t('proxy.config.create')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className='max-h-screen overflow-auto'>
|
||||
<DialogContent className="max-h-[90vh] overflow-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('proxy.config.create_proxy')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('proxy.config.create_proxy_description')}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t('proxy.config.create_proxy_description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ProxyConfigMutateForm {...props} />
|
||||
</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 [newClientID, setNewClientID] = 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 [proxyType, setProxyType] = useState<ProxyType>('http')
|
||||
const [selectedServer, setSelectedServer] = useState<Server | undefined>()
|
||||
const supportedProxyTypes: ProxyType[] = ["http", "tcp", "udp"]
|
||||
const supportedProxyTypes: ProxyType[] = ['http', 'tcp', 'udp']
|
||||
|
||||
const createProxyConfigMutation = useMutation({
|
||||
mutationKey: ['createProxyConfig', newClientID, newServerID],
|
||||
mutationFn: () => createProxyConfig({
|
||||
clientId: newClientID!,
|
||||
serverId: newServerID!,
|
||||
config: ObjToUint8Array({
|
||||
proxies: proxyConfigs
|
||||
} as ClientConfig),
|
||||
overwrite,
|
||||
}),
|
||||
mutationFn: () =>
|
||||
createProxyConfig({
|
||||
clientId: newClientID!,
|
||||
serverId: newServerID!,
|
||||
config: ObjToUint8Array({
|
||||
proxies: proxyConfigs,
|
||||
} as ClientConfig),
|
||||
overwrite,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast(t('proxy.config.create_success'))
|
||||
$proxyTableRefetchTrigger.set(Math.random())
|
||||
onSuccess?.()
|
||||
},
|
||||
onError: (e) => {
|
||||
toast(t('proxy.config.create_failed'), {
|
||||
description: JSON.stringify(e),
|
||||
})
|
||||
$proxyTableRefetchTrigger.set(Math.random())
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,27 +123,39 @@ export const ProxyConfigMutateForm = ({ overwrite, defaultProxyConfig, defaultOr
|
||||
<BaseSelector
|
||||
dataList={supportedProxyTypes.map((type) => ({ value: type, label: type }))}
|
||||
value={proxyType}
|
||||
setValue={(value) => { setProxyType(value as ProxyType) }}
|
||||
setValue={(value) => {
|
||||
setProxyType(value as ProxyType)
|
||||
}}
|
||||
/>
|
||||
{proxyConfigs && selectedServer && proxyConfigs.length > 0 &&
|
||||
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]} />
|
||||
{proxyConfigs &&
|
||||
selectedServer &&
|
||||
proxyConfigs.length > 0 &&
|
||||
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>
|
||||
}
|
||||
)}
|
||||
<Label>{t('proxy.config.proxy_name')} </Label>
|
||||
<Input className='text-sm' defaultValue={proxyName} onChange={(e) => setProxyName(e.target.value)} disabled={disableChangeProxyName} />
|
||||
{proxyName && newClientID && newServerID && <TypedProxyForm
|
||||
serverID={newServerID}
|
||||
clientID={newClientID}
|
||||
proxyName={proxyName}
|
||||
defaultProxyConfig={proxyConfigs && proxyConfigs.length > 0 ? proxyConfigs[0] : undefined}
|
||||
clientProxyConfigs={proxyConfigs}
|
||||
setClientProxyConfigs={setProxyConfigs}
|
||||
enablePreview={false}
|
||||
/>}
|
||||
<Input
|
||||
className="text-sm"
|
||||
defaultValue={proxyName}
|
||||
onChange={(e) => setProxyName(e.target.value)}
|
||||
disabled={disableChangeProxyName}
|
||||
/>
|
||||
{proxyName && newClientID && newServerID && (
|
||||
<TypedProxyForm
|
||||
serverID={newServerID}
|
||||
clientID={newClientID}
|
||||
proxyName={proxyName}
|
||||
defaultProxyConfig={proxyConfigs && proxyConfigs.length > 0 ? proxyConfigs[0] : undefined}
|
||||
clientProxyConfigs={proxyConfigs}
|
||||
setClientProxyConfigs={setProxyConfigs}
|
||||
enablePreview={false}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
disabled={!TypedProxyConfigValid(proxyConfigs[0])}
|
||||
onClick={() => {
|
||||
@@ -145,7 +164,10 @@ export const ProxyConfigMutateForm = ({ overwrite, defaultProxyConfig, defaultOr
|
||||
return
|
||||
}
|
||||
createProxyConfigMutation.mutate()
|
||||
}} >{t('proxy.config.submit')}</Button>
|
||||
}}
|
||||
>
|
||||
{t('proxy.config.submit')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -134,7 +134,7 @@ export function ProxyConfigActions({ serverID, clientID, name, row }: ProxyConfi
|
||||
return (
|
||||
<>
|
||||
<Dialog open={proxyMutateFormOpen} onOpenChange={setProxyMutateFormOpen}>
|
||||
<DialogContent className="max-h-screen overflow-auto">
|
||||
<DialogContent className="max-h-[90vh] overflow-auto">
|
||||
<ProxyConfigMutateForm
|
||||
disableChangeProxyName
|
||||
defaultProxyConfig={JSON.parse(row.original.config || '{}') as TypedProxyConfig}
|
||||
|
@@ -26,16 +26,6 @@ export type 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',
|
||||
header: function Header() {
|
||||
@@ -56,6 +46,16 @@ export const columns: ColumnDef<ProxyConfigTableSchema>[] = [
|
||||
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',
|
||||
header: function Header() {
|
||||
|
53
www/components/ui/tabs.tsx
Normal file
53
www/components/ui/tabs.tsx
Normal 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 }
|
219
www/components/worker/client_deploy.tsx
Normal file
219
www/components/worker/client_deploy.tsx
Normal 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>
|
||||
)
|
||||
}
|
22
www/components/worker/code_editor.tsx
Normal file
22
www/components/worker/code_editor.tsx
Normal 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>
|
||||
)
|
||||
}
|
127
www/components/worker/edit.tsx
Normal file
127
www/components/worker/edit.tsx
Normal 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>
|
||||
)
|
||||
}
|
104
www/components/worker/info_card.tsx
Normal file
104
www/components/worker/info_card.tsx
Normal 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>
|
||||
)
|
||||
}
|
363
www/components/worker/ingress_section.tsx
Normal file
363
www/components/worker/ingress_section.tsx
Normal 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>
|
||||
)
|
||||
}
|
25
www/components/worker/template_edit.tsx
Normal file
25
www/components/worker/template_edit.tsx
Normal 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>
|
||||
)
|
||||
}
|
23
www/components/worker/template_editor.tsx
Normal file
23
www/components/worker/template_editor.tsx
Normal 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>
|
||||
)
|
||||
}
|
116
www/components/worker/worker_create_dialog.tsx
Normal file
116
www/components/worker/worker_create_dialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
183
www/components/worker/worker_item.tsx
Normal file
183
www/components/worker/worker_item.tsx
Normal 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>
|
||||
)
|
||||
}
|
77
www/components/worker/worker_list.tsx
Normal file
77
www/components/worker/worker_list.tsx
Normal 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} />
|
||||
}
|
233
www/components/worker/worker_status.tsx
Normal file
233
www/components/worker/worker_status.tsx
Normal 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>
|
||||
)
|
||||
}
|
@@ -7,6 +7,7 @@ import {
|
||||
ChartNetworkIcon,
|
||||
Scroll,
|
||||
Cable,
|
||||
SquareFunction,
|
||||
} from "lucide-react"
|
||||
import { TbBuildingTunnel } from "react-icons/tb"
|
||||
|
||||
@@ -61,4 +62,9 @@ export const getNavItems = (t: any) => [
|
||||
url: "/console",
|
||||
icon: SquareTerminal,
|
||||
},
|
||||
{
|
||||
title: t('nav.workers'),
|
||||
url: "/workers",
|
||||
icon: SquareFunction,
|
||||
},
|
||||
]
|
||||
|
@@ -177,6 +177,13 @@
|
||||
"register": "Register",
|
||||
"userInfo": "User Information",
|
||||
"logout": "Logout",
|
||||
"add": "Add",
|
||||
"back": "Back",
|
||||
"saving": "Saving...",
|
||||
"save": "Save",
|
||||
"success": "Success",
|
||||
"failed": "Failed",
|
||||
"cancel": "Cancel",
|
||||
"clientType": "Client Type",
|
||||
"disconnect": "Disconnect",
|
||||
"connect": "Connect"
|
||||
@@ -299,9 +306,9 @@
|
||||
"title": "Tunnel Operation"
|
||||
},
|
||||
"item": {
|
||||
"client_id": "Client ID",
|
||||
"proxy_name": "Tunnel Name",
|
||||
"proxy_type": "Tunnel Type",
|
||||
"client_id": "Client ID",
|
||||
"server_id": "Server ID",
|
||||
"status": "Status",
|
||||
"visit_preview": "Visit Preview"
|
||||
@@ -470,6 +477,113 @@
|
||||
"cancel": "Cancel",
|
||||
"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": {
|
||||
"clients": "Clients",
|
||||
"servers": "Servers",
|
||||
@@ -478,7 +592,8 @@
|
||||
"editServer": "Edit Server",
|
||||
"trafficStats": "Traffic Stats",
|
||||
"realTimeLog": "Real-time Log",
|
||||
"console": "Console"
|
||||
"console": "Console",
|
||||
"workers": "Functions"
|
||||
},
|
||||
"frpc_form": {
|
||||
"add": "Add Client",
|
||||
|
@@ -23,11 +23,8 @@
|
||||
"streamlog": "Stream Log",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Information",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"newWindow": "New Window"
|
||||
@@ -121,5 +118,9 @@
|
||||
"toggle": "Toggle Language",
|
||||
"zh": "Chinese",
|
||||
"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
Reference in New Issue
Block a user