feat: add standalone controlplane support
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package controlplane
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cedar2025/xboard-node/internal/config"
|
||||
"github.com/cedar2025/xboard-node/internal/model"
|
||||
)
|
||||
|
||||
type LocalControlPlane struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewLocalControlPlane(cfg *config.Config) *LocalControlPlane { return &LocalControlPlane{cfg: cfg} }
|
||||
func (l *LocalControlPlane) SupportsPolling() bool { return false }
|
||||
func (l *LocalControlPlane) SupportsDiscovery() bool { return false }
|
||||
func (l *LocalControlPlane) SupportsReporting() bool { return false }
|
||||
func (l *LocalControlPlane) SupportsDeviceReports() bool { return false }
|
||||
|
||||
func (l *LocalControlPlane) Initial(ctx context.Context, _ func() map[string]interface{}, _ chan<- Event, _ chan<- StatusChange) (Bootstrap, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return Bootstrap{}, ctx.Err()
|
||||
default:
|
||||
}
|
||||
return Bootstrap{
|
||||
PushInterval: l.cfg.Node.PushInterval,
|
||||
PullInterval: l.cfg.Node.PullInterval,
|
||||
Config: model.NodeSpecFromStandalone(l.cfg),
|
||||
Users: model.UserSpecsFromStandalone(l.cfg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LocalControlPlane) Poll(ctx context.Context) (Snapshot, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return Snapshot{}, ctx.Err()
|
||||
default:
|
||||
return Snapshot{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LocalControlPlane) Discover(ctx context.Context, _ func() map[string]interface{}, _ chan<- Event, _ chan<- StatusChange) (PushClient, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LocalControlPlane) Report(payload ReportPayload) error { _ = payload; return nil }
|
||||
func (l *LocalControlPlane) ReportDevices(push PushClient, devices map[int][]string) {
|
||||
_, _ = push, devices
|
||||
}
|
||||
func (l *LocalControlPlane) Metrics() APIMetrics { return APIMetrics{} }
|
||||
@@ -0,0 +1,32 @@
|
||||
package controlplane
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cedar2025/xboard-node/internal/config"
|
||||
)
|
||||
|
||||
func TestLocalControlPlaneInitial(t *testing.T) {
|
||||
cp := NewLocalControlPlane(&config.Config{
|
||||
Kernel: config.KernelConfig{Type: "singbox", LogLevel: "warn"},
|
||||
Standalone: &config.StandaloneConfig{
|
||||
Enabled: true,
|
||||
Node: config.StandaloneNodeConfig{Protocol: "shadowsocks", ServerPort: 8388, Cipher: "aes-128-gcm"},
|
||||
Users: []config.StandaloneUser{{ID: 1, UUID: "secret"}},
|
||||
},
|
||||
})
|
||||
bootstrap, err := cp.Initial(context.Background(), nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Initial: %v", err)
|
||||
}
|
||||
if bootstrap.Push != nil {
|
||||
t.Fatal("local control plane should not create push client")
|
||||
}
|
||||
if bootstrap.Config == nil || bootstrap.Config.Protocol != "shadowsocks" {
|
||||
t.Fatalf("config: %#v", bootstrap.Config)
|
||||
}
|
||||
if len(bootstrap.Users) != 1 || bootstrap.Users[0].UUID != "secret" {
|
||||
t.Fatalf("users: %#v", bootstrap.Users)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package controlplane
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cedar2025/xboard-node/internal/config"
|
||||
"github.com/cedar2025/xboard-node/internal/model"
|
||||
"github.com/cedar2025/xboard-node/internal/nlog"
|
||||
"github.com/cedar2025/xboard-node/internal/panel"
|
||||
)
|
||||
|
||||
type PanelControlPlane struct {
|
||||
cfg config.PanelConfig
|
||||
wsCfg config.WSConfig
|
||||
client *panel.Client
|
||||
}
|
||||
|
||||
type panelPushClient struct {
|
||||
inner *panel.WSClient
|
||||
}
|
||||
|
||||
func NewPanelControlPlane(panelCfg config.PanelConfig, wsCfg config.WSConfig) *PanelControlPlane {
|
||||
return &PanelControlPlane{
|
||||
cfg: panelCfg,
|
||||
wsCfg: wsCfg,
|
||||
client: panel.NewClient(panelCfg),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PanelControlPlane) SupportsPolling() bool { return true }
|
||||
func (p *PanelControlPlane) SupportsDiscovery() bool { return true }
|
||||
func (p *PanelControlPlane) SupportsReporting() bool { return true }
|
||||
func (p *PanelControlPlane) SupportsDeviceReports() bool { return true }
|
||||
|
||||
func (p *PanelControlPlane) Initial(ctx context.Context, metricsFn func() map[string]interface{}, events chan<- Event, statuses chan<- StatusChange) (Bootstrap, error) {
|
||||
hs, err := p.client.Handshake()
|
||||
if err != nil {
|
||||
return Bootstrap{}, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
|
||||
bootstrap := Bootstrap{PushInterval: hs.Settings.PushInterval, PullInterval: hs.Settings.PullInterval}
|
||||
if hs.WebSocket.Enabled && hs.WebSocket.WSURL != "" {
|
||||
bootstrap.Push = p.newPushClient(metricsFn, events, statuses, hs.WebSocket.WSURL)
|
||||
return bootstrap, nil
|
||||
}
|
||||
|
||||
nlog.Core().Info("websocket disabled, using REST API")
|
||||
configSnapshot, err := p.client.GetConfig()
|
||||
if err != nil {
|
||||
return Bootstrap{}, fmt.Errorf("initial config fetch: %w", err)
|
||||
}
|
||||
if configSnapshot == nil {
|
||||
return Bootstrap{}, fmt.Errorf("initial config is nil")
|
||||
}
|
||||
users, err := p.client.GetUsers()
|
||||
if err != nil {
|
||||
return Bootstrap{}, fmt.Errorf("initial user fetch: %w", err)
|
||||
}
|
||||
bootstrap.Config = model.NodeSpecFromPanel(configSnapshot)
|
||||
bootstrap.Users = model.UserSpecsFromPanel(users)
|
||||
return bootstrap, nil
|
||||
}
|
||||
|
||||
func (p *PanelControlPlane) Poll(ctx context.Context) (Snapshot, error) {
|
||||
configSnapshot, err := p.client.GetConfig()
|
||||
if err != nil {
|
||||
return Snapshot{}, fmt.Errorf("poll config: %w", err)
|
||||
}
|
||||
users, err := p.client.GetUsers()
|
||||
if err != nil {
|
||||
return Snapshot{}, fmt.Errorf("poll users: %w", err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return Snapshot{}, ctx.Err()
|
||||
default:
|
||||
}
|
||||
return Snapshot{Config: model.NodeSpecFromPanel(configSnapshot), Users: model.UserSpecsFromPanel(users)}, nil
|
||||
}
|
||||
|
||||
func (p *PanelControlPlane) Discover(ctx context.Context, metricsFn func() map[string]interface{}, events chan<- Event, statuses chan<- StatusChange) (PushClient, error) {
|
||||
hs, err := p.client.Handshake()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handshake: %w", err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
if hs.WebSocket.Enabled && hs.WebSocket.WSURL != "" {
|
||||
return p.newPushClient(metricsFn, events, statuses, hs.WebSocket.WSURL), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *PanelControlPlane) Report(payload ReportPayload) error {
|
||||
return p.client.Report(payload.Traffic, payload.Alive, payload.Online, payload.CPU, payload.Mem, payload.Swap, payload.Disk, payload.Metrics)
|
||||
}
|
||||
|
||||
func (p *PanelControlPlane) ReportDevices(push PushClient, devices map[int][]string) {
|
||||
if push != nil {
|
||||
push.SendDeviceReport(devices)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PanelControlPlane) Metrics() APIMetrics {
|
||||
api := p.client.SnapshotMetrics()
|
||||
return APIMetrics{Success: api.Success, Failure: api.Failure}
|
||||
}
|
||||
|
||||
func (p *PanelControlPlane) newPushClient(metricsFn func() map[string]interface{}, events chan<- Event, statuses chan<- StatusChange, wsURL string) PushClient {
|
||||
cfg := panel.WSClientConfig{
|
||||
StatusInterval: time.Duration(p.wsCfg.StatusInterval) * time.Second,
|
||||
HandshakeTimeout: time.Duration(p.wsCfg.HandshakeTimeout) * time.Second,
|
||||
BackoffInitial: time.Duration(p.wsCfg.BackoffInitial) * time.Second,
|
||||
BackoffMax: time.Duration(p.wsCfg.BackoffMax) * time.Second,
|
||||
}
|
||||
inner := panel.NewWSClient(
|
||||
wsURL,
|
||||
p.cfg.Token,
|
||||
p.cfg.NodeID,
|
||||
cfg,
|
||||
func(event panel.WSEvent) {
|
||||
translated := translateWSEvent(event)
|
||||
select {
|
||||
case events <- translated:
|
||||
default:
|
||||
nlog.Core().Warn("ws event channel full, dropping event", "type", translated.Type)
|
||||
}
|
||||
},
|
||||
func(status panel.WSStatusChange) {
|
||||
select {
|
||||
case statuses <- StatusChange{Connected: status.Connected}:
|
||||
default:
|
||||
}
|
||||
},
|
||||
metricsFn,
|
||||
)
|
||||
return &panelPushClient{inner: inner}
|
||||
}
|
||||
|
||||
func translateWSEvent(event panel.WSEvent) Event {
|
||||
translated := Event{Type: EventType(event.Type), DeltaAction: event.DeltaAction, DeviceUsers: event.DeviceUsers}
|
||||
if event.Config != nil {
|
||||
translated.Config = model.NodeSpecFromPanel(event.Config)
|
||||
}
|
||||
if event.Users != nil {
|
||||
translated.Users = model.UserSpecsFromPanel(event.Users)
|
||||
}
|
||||
if event.DeltaUsers != nil {
|
||||
translated.DeltaUsers = model.UserSpecsFromPanel(event.DeltaUsers)
|
||||
}
|
||||
return translated
|
||||
}
|
||||
|
||||
func (p *panelPushClient) Run(ctx context.Context) { p.inner.Run(ctx) }
|
||||
func (p *panelPushClient) IsConnected() bool { return p.inner.IsConnected() }
|
||||
func (p *panelPushClient) SendDeviceReport(devices map[int][]string) { p.inner.SendDeviceReport(devices) }
|
||||
@@ -0,0 +1,85 @@
|
||||
package controlplane
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cedar2025/xboard-node/internal/model"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventSyncConfig EventType = "sync.config"
|
||||
EventSyncUsers EventType = "sync.users"
|
||||
EventSyncUserDelta EventType = "sync.user.delta"
|
||||
EventSyncDevices EventType = "sync.devices"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type EventType
|
||||
Config *model.NodeSpec
|
||||
Users []model.UserSpec
|
||||
DeltaAction string
|
||||
DeltaUsers []model.UserSpec
|
||||
DeviceUsers map[int][]string
|
||||
}
|
||||
|
||||
type StatusChange struct {
|
||||
Connected bool
|
||||
}
|
||||
|
||||
type APIMetrics struct {
|
||||
Success uint64
|
||||
Failure uint64
|
||||
}
|
||||
|
||||
type Bootstrap struct {
|
||||
PushInterval int
|
||||
PullInterval int
|
||||
Push PushClient
|
||||
Config *model.NodeSpec
|
||||
Users []model.UserSpec
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Config *model.NodeSpec
|
||||
Users []model.UserSpec
|
||||
}
|
||||
|
||||
type ReportPayload struct {
|
||||
Traffic map[int][2]int64
|
||||
Alive map[int][]string
|
||||
Online map[int]int
|
||||
CPU float64
|
||||
Mem [2]uint64
|
||||
Swap [2]uint64
|
||||
Disk [2]uint64
|
||||
Metrics map[string]interface{}
|
||||
}
|
||||
|
||||
type PushClient interface {
|
||||
Run(ctx context.Context)
|
||||
IsConnected() bool
|
||||
SendDeviceReport(devices map[int][]string)
|
||||
}
|
||||
|
||||
type Source interface {
|
||||
Initial(ctx context.Context, metricsFn func() map[string]interface{}, events chan<- Event, statuses chan<- StatusChange) (Bootstrap, error)
|
||||
Poll(ctx context.Context) (Snapshot, error)
|
||||
Discover(ctx context.Context, metricsFn func() map[string]interface{}, events chan<- Event, statuses chan<- StatusChange) (PushClient, error)
|
||||
Metrics() APIMetrics
|
||||
SupportsPolling() bool
|
||||
SupportsDiscovery() bool
|
||||
}
|
||||
|
||||
type Sink interface {
|
||||
Report(payload ReportPayload) error
|
||||
ReportDevices(push PushClient, devices map[int][]string)
|
||||
SupportsReporting() bool
|
||||
SupportsDeviceReports() bool
|
||||
}
|
||||
|
||||
type ControlPlane interface {
|
||||
Source
|
||||
Sink
|
||||
}
|
||||
Reference in New Issue
Block a user