diff --git a/atsf_agent/agent.json b/atsf_agent/agent.json index 039bc812..cdbb0349 100644 --- a/atsf_agent/agent.json +++ b/atsf_agent/agent.json @@ -1,14 +1,10 @@ { "server_url": "http://127.0.0.1:3000", "agent_token": "123", - "node_name": "edge-01", - "node_ip": "10.0.0.8", - "agent_version": "0.1.0", - "nginx_version": "1.25.5", "data_dir": "./data", "nginx_container_name": "atsflare-nginx", "nginx_docker_image": "nginx:stable-alpine", - "heartbeat_interval": 30000000000, - "sync_interval": 30000000000, - "request_timeout": 10000000000 + "heartbeat_interval": 30000, + "sync_interval": 30000, + "request_timeout": 10000 } diff --git a/atsf_agent/cmd/agent/main.go b/atsf_agent/cmd/agent/main.go index d7e95a4e..ec24d939 100644 --- a/atsf_agent/cmd/agent/main.go +++ b/atsf_agent/cmd/agent/main.go @@ -24,9 +24,21 @@ func main() { if err != nil { log.Fatal(err) } + cfg.NginxVersion = nginx.DetectVersion( + context.Background(), + nginx.ExecutorOptions{ + NginxPath: cfg.NginxPath, + DockerBinary: cfg.DockerBinary, + ContainerName: cfg.NginxContainerName, + Image: cfg.NginxDockerImage, + RouteConfigPath: cfg.RouteConfigPath, + CertDir: cfg.CertDir, + NginxCertDir: cfg.NginxCertDir, + }, + ) log.Printf("agent config loaded: server=%s node=%s ip=%s heartbeat_interval=%s sync_interval=%s route_config=%s cert_dir=%s", cfg.ServerURL, cfg.NodeName, cfg.NodeIP, cfg.HeartbeatInterval, cfg.SyncInterval, cfg.RouteConfigPath, cfg.CertDir) - client := httpclient.New(cfg.ServerURL, cfg.AgentToken, cfg.RequestTimeout) + client := httpclient.New(cfg.ServerURL, cfg.AgentToken, cfg.RequestTimeout.Duration()) stateStore := state.NewStore(cfg.StatePath) runner := &agent.Runner{ Config: cfg, diff --git a/atsf_agent/internal/agent/runner.go b/atsf_agent/internal/agent/runner.go index 37d01fdd..b7c03358 100644 --- a/atsf_agent/internal/agent/runner.go +++ b/atsf_agent/internal/agent/runner.go @@ -52,9 +52,9 @@ func (r *Runner) Run(ctx context.Context) error { log.Printf("agent initial discovery register failed: %v", err) } - heartbeatTicker := time.NewTicker(r.Config.HeartbeatInterval) + heartbeatTicker := time.NewTicker(r.Config.HeartbeatInterval.Duration()) defer heartbeatTicker.Stop() - syncTicker := time.NewTicker(r.Config.SyncInterval) + syncTicker := time.NewTicker(r.Config.SyncInterval.Duration()) defer syncTicker.Stop() for { diff --git a/atsf_agent/internal/agent/runner_test.go b/atsf_agent/internal/agent/runner_test.go index 6e66e775..194bd7c5 100644 --- a/atsf_agent/internal/agent/runner_test.go +++ b/atsf_agent/internal/agent/runner_test.go @@ -102,10 +102,10 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) { AgentToken: "agent-token", NodeName: "edge-01", NodeIP: "10.0.0.8", - AgentVersion: "0.1.0", + AgentVersion: config.AgentVersion, NginxVersion: "1.25.5", - HeartbeatInterval: 10 * time.Millisecond, - SyncInterval: 20 * time.Millisecond, + HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), + SyncInterval: config.MillisecondDuration(20 * time.Millisecond), }, StateStore: stateStore, HeartbeatService: heartbeatService, @@ -153,10 +153,10 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) { AgentToken: "agent-token", NodeName: "edge-01", NodeIP: "10.0.0.8", - AgentVersion: "0.1.0", + AgentVersion: config.AgentVersion, NginxVersion: "1.25.5", - HeartbeatInterval: 10 * time.Millisecond, - SyncInterval: 10 * time.Millisecond, + HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), + SyncInterval: config.MillisecondDuration(10 * time.Millisecond), }, StateStore: stateStore, HeartbeatService: heartbeatService, @@ -214,20 +214,20 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) { DiscoveryToken: cfg.DiscoveryToken, NodeName: cfg.NodeName, NodeIP: cfg.NodeIP, - AgentVersion: "0.1.0", + AgentVersion: config.AgentVersion, NginxVersion: "1.25.5", - HeartbeatInterval: 10 * time.Millisecond, - SyncInterval: 20 * time.Millisecond, + HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), + SyncInterval: config.MillisecondDuration(20 * time.Millisecond), }, StateStore: stateStore, HeartbeatService: heartbeatService, SyncService: syncService, } runner.Config = cfg - runner.Config.AgentVersion = "0.1.0" + runner.Config.AgentVersion = config.AgentVersion runner.Config.NginxVersion = "1.25.5" - runner.Config.HeartbeatInterval = 10 * time.Millisecond - runner.Config.SyncInterval = 20 * time.Millisecond + runner.Config.HeartbeatInterval = config.MillisecondDuration(10 * time.Millisecond) + runner.Config.SyncInterval = config.MillisecondDuration(20 * time.Millisecond) err = runner.Run(ctx) if !errors.Is(err, context.Canceled) { diff --git a/atsf_agent/internal/config/config.go b/atsf_agent/internal/config/config.go index e7505658..315faa7a 100644 --- a/atsf_agent/internal/config/config.go +++ b/atsf_agent/internal/config/config.go @@ -19,25 +19,25 @@ const ( ) type Config struct { - ServerURL string `json:"server_url"` - AgentToken string `json:"agent_token"` - DiscoveryToken string `json:"discovery_token"` - NodeName string `json:"node_name"` - NodeIP string `json:"node_ip"` - AgentVersion string `json:"agent_version"` - NginxVersion string `json:"nginx_version"` - NginxPath string `json:"nginx_path"` - NginxContainerName string `json:"nginx_container_name"` - NginxDockerImage string `json:"nginx_docker_image"` - DockerBinary string `json:"docker_binary"` - DataDir string `json:"data_dir"` - RouteConfigPath string `json:"route_config_path"` - CertDir string `json:"cert_dir"` - NginxCertDir string `json:"nginx_cert_dir"` - StatePath string `json:"state_path"` - HeartbeatInterval time.Duration `json:"heartbeat_interval"` - SyncInterval time.Duration `json:"sync_interval"` - RequestTimeout time.Duration `json:"request_timeout"` + ServerURL string `json:"server_url"` + AgentToken string `json:"agent_token"` + DiscoveryToken string `json:"discovery_token"` + NodeName string `json:"node_name"` + NodeIP string `json:"node_ip"` + AgentVersion string `json:"-"` + NginxVersion string `json:"-"` + NginxPath string `json:"nginx_path"` + NginxContainerName string `json:"nginx_container_name"` + NginxDockerImage string `json:"nginx_docker_image"` + DockerBinary string `json:"docker_binary"` + DataDir string `json:"data_dir"` + RouteConfigPath string `json:"route_config_path"` + CertDir string `json:"cert_dir"` + NginxCertDir string `json:"nginx_cert_dir"` + StatePath string `json:"state_path"` + HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"` + SyncInterval MillisecondDuration `json:"sync_interval"` + RequestTimeout MillisecondDuration `json:"request_timeout"` configPath string } @@ -60,9 +60,7 @@ func Load(path string) (*Config, error) { func applyDefaults(cfg *Config, baseDir string) { baseDir = filepath.Clean(baseDir) - if cfg.AgentVersion == "" { - cfg.AgentVersion = "dev" - } + cfg.AgentVersion = AgentVersion if cfg.NginxContainerName == "" { cfg.NginxContainerName = "atsflare-nginx" } @@ -103,13 +101,13 @@ func applyDefaults(cfg *Config, baseDir string) { } } if cfg.HeartbeatInterval <= 0 { - cfg.HeartbeatInterval = 30 * time.Second + cfg.HeartbeatInterval = MillisecondDuration(30 * time.Second) } if cfg.SyncInterval <= 0 { - cfg.SyncInterval = 30 * time.Second + cfg.SyncInterval = MillisecondDuration(30 * time.Second) } if cfg.RequestTimeout <= 0 { - cfg.RequestTimeout = 10 * time.Second + cfg.RequestTimeout = MillisecondDuration(10 * time.Second) } normalizeManagedPaths(cfg) } diff --git a/atsf_agent/internal/config/config_test.go b/atsf_agent/internal/config/config_test.go index 41657d96..780be26c 100644 --- a/atsf_agent/internal/config/config_test.go +++ b/atsf_agent/internal/config/config_test.go @@ -5,17 +5,17 @@ import ( "os" "path/filepath" "testing" + "time" ) func TestLoadDockerModeUsesManagedPaths(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "agent.json") payload := map[string]any{ - "server_url": "http://127.0.0.1:3000", - "agent_token": "token", - "node_name": "edge-01", - "node_ip": "10.0.0.8", - "agent_version": "0.1.0", + "server_url": "http://127.0.0.1:3000", + "agent_token": "token", + "node_name": "edge-01", + "node_ip": "10.0.0.8", } data, err := json.Marshal(payload) if err != nil { @@ -87,12 +87,11 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "agent.json") payload := map[string]any{ - "server_url": "http://127.0.0.1:3000", - "agent_token": "token", - "node_name": "edge-01", - "node_ip": "10.0.0.8", - "data_dir": "/srv/atsflare", - "agent_version": "0.1.0", + "server_url": "http://127.0.0.1:3000", + "agent_token": "token", + "node_name": "edge-01", + "node_ip": "10.0.0.8", + "data_dir": "/srv/atsflare", } data, err := json.Marshal(payload) if err != nil { @@ -117,3 +116,84 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { t.Fatalf("unexpected cert dir: %s", cfg.CertDir) } } + +func TestLoadUsesMillisecondsForIntervals(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "agent.json") + payload := map[string]any{ + "server_url": "http://127.0.0.1:3000", + "agent_token": "token", + "node_name": "edge-01", + "node_ip": "10.0.0.8", + "heartbeat_interval": 30000, + "sync_interval": 45000, + "request_timeout": 1500, + } + data, err := json.Marshal(payload) + if err != nil { + t.Fatalf("failed to marshal config: %v", err) + } + if err = os.WriteFile(configPath, data, 0o644); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + cfg, err := Load(configPath) + if err != nil { + t.Fatalf("Load failed: %v", err) + } + + if cfg.HeartbeatInterval.Duration() != 30*time.Second { + t.Fatalf("unexpected heartbeat interval: %s", cfg.HeartbeatInterval) + } + if cfg.SyncInterval.Duration() != 45*time.Second { + t.Fatalf("unexpected sync interval: %s", cfg.SyncInterval) + } + if cfg.RequestTimeout.Duration() != 1500*time.Millisecond { + t.Fatalf("unexpected request timeout: %s", cfg.RequestTimeout) + } +} + +func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "agent.json") + if err := os.WriteFile(configPath, []byte(`{"server_url":"http://127.0.0.1:3000","agent_token":"token","node_name":"edge-01","node_ip":"10.0.0.8"}`), 0o644); err != nil { + t.Fatalf("failed to write config: %v", err) + } + + cfg, err := Load(configPath) + if err != nil { + t.Fatalf("Load failed: %v", err) + } + cfg.NginxVersion = "1.25.5" + cfg.HeartbeatInterval = MillisecondDuration(5 * time.Second) + cfg.SyncInterval = MillisecondDuration(6 * time.Second) + cfg.RequestTimeout = MillisecondDuration(7 * time.Second) + + if err = cfg.Save(); err != nil { + t.Fatalf("Save failed: %v", err) + } + + data, err := os.ReadFile(configPath) + if err != nil { + t.Fatalf("failed to read saved config: %v", err) + } + var decoded map[string]any + if err = json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("failed to decode saved config: %v", err) + } + if _, ok := decoded["agent_version"]; ok { + t.Fatal("agent_version should not be persisted") + } + if _, ok := decoded["nginx_version"]; ok { + t.Fatal("nginx_version should not be persisted") + } + if decoded["heartbeat_interval"] != float64(5000) { + t.Fatalf("unexpected heartbeat interval: %#v", decoded["heartbeat_interval"]) + } + if decoded["sync_interval"] != float64(6000) { + t.Fatalf("unexpected sync interval: %#v", decoded["sync_interval"]) + } + if decoded["request_timeout"] != float64(7000) { + t.Fatalf("unexpected request timeout: %#v", decoded["request_timeout"]) + } +} diff --git a/atsf_agent/internal/config/duration.go b/atsf_agent/internal/config/duration.go new file mode 100644 index 00000000..17ca3f87 --- /dev/null +++ b/atsf_agent/internal/config/duration.go @@ -0,0 +1,54 @@ +package config + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" +) + +type MillisecondDuration time.Duration + +func (d MillisecondDuration) Duration() time.Duration { + return time.Duration(d) +} + +func (d MillisecondDuration) String() string { + return time.Duration(d).String() +} + +func (d *MillisecondDuration) UnmarshalJSON(data []byte) error { + raw := strings.TrimSpace(string(data)) + if raw == "" || raw == "null" { + *d = 0 + return nil + } + if strings.HasPrefix(raw, "\"") { + var text string + if err := json.Unmarshal(data, &text); err != nil { + return err + } + text = strings.TrimSpace(text) + if text == "" { + *d = 0 + return nil + } + parsed, err := time.ParseDuration(text) + if err != nil { + return fmt.Errorf("invalid duration string %q: %w", text, err) + } + *d = MillisecondDuration(parsed) + return nil + } + ms, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return fmt.Errorf("invalid duration milliseconds %q: %w", raw, err) + } + *d = MillisecondDuration(time.Duration(ms) * time.Millisecond) + return nil +} + +func (d MillisecondDuration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(d).Milliseconds()) +} diff --git a/atsf_agent/internal/config/version.go b/atsf_agent/internal/config/version.go new file mode 100644 index 00000000..4e3bf950 --- /dev/null +++ b/atsf_agent/internal/config/version.go @@ -0,0 +1,3 @@ +package config + +const AgentVersion = "0.1.0" diff --git a/atsf_agent/internal/nginx/manager.go b/atsf_agent/internal/nginx/manager.go index 2678270e..22f0c88d 100644 --- a/atsf_agent/internal/nginx/manager.go +++ b/atsf_agent/internal/nginx/manager.go @@ -263,6 +263,56 @@ func NewExecutor(options ExecutorOptions) Executor { } } +func DetectVersion(ctx context.Context, options ExecutorOptions) string { + version, err := detectVersion(ctx, options, &OSCommandRunner{}) + if err != nil { + log.Printf("detect nginx version failed: %v", err) + return "" + } + log.Printf("detected nginx version: %s", version) + return version +} + +func detectVersion(ctx context.Context, options ExecutorOptions, runner CommandRunner) (string, error) { + if runner == nil { + runner = &OSCommandRunner{} + } + if options.NginxPath != "" { + output, err := runner.Run(ctx, options.NginxPath, "-v") + if err != nil { + return "", fmt.Errorf("run nginx -v failed: %w: %s", err, string(output)) + } + version := parseNginxVersion(string(output)) + if version == "" { + return "", errors.New("cannot parse nginx version from binary output") + } + return version, nil + } + output, err := runner.Run(ctx, options.DockerBinary, "run", "--rm", options.Image, "nginx", "-v") + if err != nil { + return "", fmt.Errorf("run docker nginx -v failed: %w: %s", err, string(output)) + } + version := parseNginxVersion(string(output)) + if version == "" { + return "", errors.New("cannot parse nginx version from docker output") + } + return version, nil +} + +func parseNginxVersion(output string) string { + start := strings.Index(output, "nginx/") + if start < 0 { + return "" + } + version := output[start+len("nginx/"):] + for i, r := range version { + if r == ' ' || r == '\n' || r == '\r' || r == '\t' { + return version[:i] + } + } + return version +} + type backupState struct { RouteExisted bool RouteData []byte diff --git a/atsf_agent/internal/nginx/manager_test.go b/atsf_agent/internal/nginx/manager_test.go index ea0a021a..17a59dfd 100644 --- a/atsf_agent/internal/nginx/manager_test.go +++ b/atsf_agent/internal/nginx/manager_test.go @@ -203,6 +203,47 @@ func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) { } } +func TestDetectVersionFromBinary(t *testing.T) { + version, err := detectVersion(context.Background(), ExecutorOptions{ + NginxPath: "/opt/nginx/sbin/nginx", + }, &fakeRunner{ + runFn: func(name string, args ...string) ([]byte, error) { + return []byte("nginx version: nginx/1.25.5\n"), nil + }, + }) + if err != nil { + t.Fatalf("detectVersion failed: %v", err) + } + if version != "1.25.5" { + t.Fatalf("unexpected version: %s", version) + } +} + +func TestDetectVersionFromDockerImage(t *testing.T) { + runner := &fakeRunner{ + runFn: func(name string, args ...string) ([]byte, error) { + return []byte("nginx version: nginx/1.27.4\n"), nil + }, + } + version, err := detectVersion(context.Background(), ExecutorOptions{ + DockerBinary: "docker", + Image: "nginx:stable-alpine", + }, runner) + if err != nil { + t.Fatalf("detectVersion failed: %v", err) + } + if version != "1.27.4" { + t.Fatalf("unexpected version: %s", version) + } + if len(runner.calls) != 1 { + t.Fatalf("expected one command call, got %d", len(runner.calls)) + } + expectedArgs := []string{"run", "--rm", "nginx:stable-alpine", "nginx", "-v"} + if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) { + t.Fatalf("unexpected docker args: %#v", runner.calls[0].args) + } +} + func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { tempDir := t.TempDir() manager := &Manager{ diff --git a/atsf_server/common/constants.go b/atsf_server/common/constants.go index 8d70c2e2..ac89b39f 100644 --- a/atsf_server/common/constants.go +++ b/atsf_server/common/constants.go @@ -9,7 +9,7 @@ import ( var StartTime = time.Now().Unix() // unit: second var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change -var SystemName = "项目模板" +var SystemName = "ATSFlare" var ServerAddress = "http://localhost:3000" var Footer = "" var HomePageLink = "" diff --git a/atsf_server/web/public/index.html b/atsf_server/web/public/index.html index ea91592b..24866467 100644 --- a/atsf_server/web/public/index.html +++ b/atsf_server/web/public/index.html @@ -9,7 +9,7 @@ name="description" content="Web site created using create-react-app" /> -