Files

199 lines
6.7 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package agent 装载 OpenFlare 边缘 agent 插件:openresty/WAF 运行时管理、
// 心跳同步、配置下发与 WebSocket 控制通道,以 Cordis 驱动形态在 profile "agent" 下运行。
package agent
import (
"context"
"errors"
"fmt"
"log/slog"
"Wavelet/core"
"Wavelet/openflare/plugins/agent/agent"
"Wavelet/openflare/plugins/agent/config"
"Wavelet/openflare/plugins/agent/geoipupdate"
"Wavelet/openflare/plugins/agent/heartbeat"
"Wavelet/openflare/plugins/agent/httpclient"
"Wavelet/openflare/plugins/agent/nginx"
"Wavelet/openflare/plugins/agent/runtimeuser"
"Wavelet/openflare/plugins/agent/state"
syncservice "Wavelet/openflare/plugins/agent/sync"
"Wavelet/openflare/plugins/agent/updater"
"Wavelet/openflare/plugins/agent/wsclient"
"Wavelet/pkg/util"
)
// DriverTypeAgent 是边缘 agent 守护进程专属的驱动类型:
// 只有 profile 与之相等时内核才会 Start/Stop 本插件。
const DriverTypeAgent core.DriverType = "agent"
// Plugin 实现 core.Plugin 与 core.Driver,承载 agent 进程的全部装配与生命周期。
type Plugin struct {
configPath string
runner *agent.Runner
geo *geoipupdate.Updater
done chan error
started bool
}
// New 创建 agent 插件,configPath 指向其 JSON 配置文件。
func New(configPath string) *Plugin {
return &Plugin{configPath: configPath, done: make(chan error, 1)}
}
// Name 返回插件标识。
func (p *Plugin) Name() string { return "agent" }
// Type 返回驱动类型。
func (p *Plugin) Type() core.DriverType { return DriverTypeAgent }
// Apply 加载配置、确保运行环境、装配各服务并注册驱动。
func (p *Plugin) Apply(ctx *core.Context) error {
cfg, err := config.Load(p.configPath)
if err != nil {
return fmt.Errorf("load agent config: %w", err)
}
if err := runtimeuser.EnsureProcessUser(); err != nil {
return fmt.Errorf("ensure runtime user: %w", err)
}
if err := runtimeuser.EnsurePathOwnership(
cfg.DataDir, runtimeuser.DefaultDirPerm, runtimeuser.DefaultFilePerm,
); err != nil {
return fmt.Errorf("ensure data dir ownership, data_dir=%s: %w", cfg.DataDir, err)
}
nginxOptions := nginx.ExecutorOptions{
NginxPath: cfg.OpenrestyPath,
MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
}
cfg.ExtVersion = nginx.DetectVersion(context.Background(), nginxOptions)
logConfigLoaded(cfg)
runtimeManager := newRuntimeManager(cfg, nginxOptions)
if err := runtimeManager.EnsureLuaAssets(); err != nil {
return fmt.Errorf("ensure managed lua assets: %w", err)
}
client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
wsClient := wsclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
stateStore := state.NewStore(cfg.StatePath)
observabilityBuffer := state.NewObservabilityBufferStore(cfg.ObservabilityBufferPath)
syncService := syncservice.New(client, runtimeManager, stateStore)
syncService.SetPagesDir(cfg.PagesDir)
heartbeatService := heartbeat.New(client)
updateService := updater.New()
p.runner = &agent.Runner{
Config: cfg,
StateStore: stateStore,
HeartbeatCycle: &heartbeat.Cycle{
Config: cfg,
StateStore: stateStore,
ObservabilityBuffer: observabilityBuffer,
Heartbeat: heartbeatService,
Sync: syncService,
Updater: updateService,
},
HeartbeatService: heartbeatService,
SyncService: syncService,
RuntimeManager: runtimeManager,
WebSocketService: wsClient,
}
p.geo = newGeoIPUpdater(cfg)
return ctx.RegisterDriver(p)
}
// Start 预热 GeoIP 库并拉起心跳/同步/控制通道主循环。
//
// runner.Run 自身阻塞至 ctx 取消,因此放到独立 goroutine 中执行,
// 由 Stop 收敛其结果——内核要求驱动 Start 不得阻塞。
func (p *Plugin) Start(ctx context.Context) error {
if err := p.geo.EnsureInitialDatabases(ctx); err != nil {
slog.Warn("failed to prepare GeoIP databases before agent startup", "error", err)
}
util.Go(func() { p.geo.Run(ctx) })
util.Go(func() { p.done <- p.runner.Run(ctx) })
p.started = true
slog.Info("agent process started")
return nil
}
// Stop 等待主循环退出并返回其结果;ctx 超时时报告超时而非静默成功。
func (p *Plugin) Stop(ctx context.Context) error {
if !p.started {
return nil
}
select {
case err := <-p.done:
p.started = false
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
slog.Info("agent process stopped")
return nil
case <-ctx.Done():
p.started = false
return fmt.Errorf("agent shutdown timeout: %w", ctx.Err())
}
}
// newGeoIPUpdater 装配国家/城市两套 mmdb 的下载与周期更新。
func newGeoIPUpdater(cfg *config.Config) *geoipupdate.Updater {
return &geoipupdate.Updater{
MMDBPath: cfg.MMDBPath,
DownloadURL: cfg.MMDBDownloadURL,
CityMMDBPath: cfg.CityMMDBPath,
CityDownloadURL: cfg.CityMMDBDownloadURL,
UpdateInterval: cfg.MMDBUpdateInterval.Duration(),
}
}
// newRuntimeManager 构造 openresty 配置与证书/Lua 资产的运行时管理器。
func newRuntimeManager(cfg *config.Config, nginxOptions nginx.ExecutorOptions) *nginx.Manager {
return &nginx.Manager{
MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath,
AccessLogPath: cfg.AccessLogPath,
CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir,
RuntimeConfigDir: cfg.RuntimeConfigDir,
MMDBPath: cfg.MMDBPath,
CityMMDBPath: cfg.CityMMDBPath,
PagesDir: cfg.PagesDir,
OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyObservabilityPort),
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
OpenrestyResolverDirective: "",
Executor: nginx.NewExecutor(nginxOptions),
}
}
func logConfigLoaded(cfg *config.Config) {
slog.Info("agent config loaded",
"server", cfg.ServerURL,
"node", cfg.NodeName,
"ip", cfg.NodeIP,
"heartbeat_interval", cfg.HeartbeatInterval,
"route_config", cfg.RouteConfigPath,
"access_log", cfg.AccessLogPath,
"cert_dir", cfg.CertDir,
"lua_dir", cfg.LuaDir,
"runtime_config_dir", cfg.RuntimeConfigDir,
"mmdb_path", cfg.MMDBPath,
"city_mmdb_path", cfg.CityMMDBPath,
)
}