Refactor Nginx references to OpenResty throughout the codebase

- Updated all instances of "nginx" to "openresty" in log messages, error messages, and comments.
- Changed paths and Docker image names to reflect OpenResty usage.
- Modified test cases to align with OpenResty commands and configurations.
- Adjusted documentation to replace Nginx mentions with OpenResty, including setup instructions and configuration details.
- Ensured that version detection and runtime commands are consistent with OpenResty.
This commit is contained in:
ryan
2026-03-12 09:48:38 +08:00
parent 22eb563939
commit 29a0c64ba3
15 changed files with 247 additions and 182 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ _✨ control plane for reverse proxy management ✨_
## Repository layout ## Repository layout
- `atsf_server`: Gin + GORM + SQLite control plane, including admin API, Agent API and Web UI - `atsf_server`: Gin + GORM + SQLite control plane, including admin API, Agent API and Web UI
- `atsf_agent`: single-binary Go Agent for registration, heartbeat, config sync and Nginx reload - `atsf_agent`: single-binary Go Agent for registration, heartbeat, config sync and OpenResty reload
- `docs`: design, development guidelines, implementation plan and deployment docs - `docs`: design, development guidelines, implementation plan and deployment docs
## Quick start ## Quick start
+2 -2
View File
@@ -2,8 +2,8 @@
"server_url": "http://127.0.0.1:3000", "server_url": "http://127.0.0.1:3000",
"agent_token": "123", "agent_token": "123",
"data_dir": "./data", "data_dir": "./data",
"nginx_container_name": "atsflare-nginx", "openresty_container_name": "atsflare-openresty",
"nginx_docker_image": "nginx:stable-alpine", "openresty_docker_image": "openresty/openresty:alpine",
"heartbeat_interval": 30000, "heartbeat_interval": 30000,
"sync_interval": 30000, "sync_interval": 30000,
"request_timeout": 10000 "request_timeout": 10000
+9 -9
View File
@@ -28,13 +28,13 @@ func main() {
cfg.NginxVersion = nginx.DetectVersion( cfg.NginxVersion = nginx.DetectVersion(
context.Background(), context.Background(),
nginx.ExecutorOptions{ nginx.ExecutorOptions{
NginxPath: cfg.NginxPath, NginxPath: cfg.OpenrestyPath,
DockerBinary: cfg.DockerBinary, DockerBinary: cfg.DockerBinary,
ContainerName: cfg.NginxContainerName, ContainerName: cfg.OpenrestyContainerName,
Image: cfg.NginxDockerImage, Image: cfg.OpenrestyDockerImage,
RouteConfigPath: cfg.RouteConfigPath, RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir, CertDir: cfg.CertDir,
NginxCertDir: cfg.NginxCertDir, NginxCertDir: cfg.OpenrestyCertDir,
}, },
) )
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) 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)
@@ -48,15 +48,15 @@ func main() {
SyncService: syncservice.New(client, &nginx.Manager{ SyncService: syncservice.New(client, &nginx.Manager{
RouteConfigPath: cfg.RouteConfigPath, RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir, CertDir: cfg.CertDir,
NginxCertDir: cfg.NginxCertDir, NginxCertDir: cfg.OpenrestyCertDir,
Executor: nginx.NewExecutor(nginx.ExecutorOptions{ Executor: nginx.NewExecutor(nginx.ExecutorOptions{
NginxPath: cfg.NginxPath, NginxPath: cfg.OpenrestyPath,
DockerBinary: cfg.DockerBinary, DockerBinary: cfg.DockerBinary,
ContainerName: cfg.NginxContainerName, ContainerName: cfg.OpenrestyContainerName,
Image: cfg.NginxDockerImage, Image: cfg.OpenrestyDockerImage,
RouteConfigPath: cfg.RouteConfigPath, RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir, CertDir: cfg.CertDir,
NginxCertDir: cfg.NginxCertDir, NginxCertDir: cfg.OpenrestyCertDir,
}), }),
}, stateStore), }, stateStore),
Updater: updater.New(), Updater: updater.New(),
+8 -8
View File
@@ -95,7 +95,7 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) {
}, },
} }
syncService := &fakeSyncService{ syncService := &fakeSyncService{
startupErr: errors.New("当前没有激活版本,保持当前 Nginx 配置"), startupErr: errors.New("当前没有激活版本,保持当前 OpenResty 配置"),
} }
runner := &Runner{ runner := &Runner{
Config: &config.Config{ Config: &config.Config{
@@ -103,7 +103,7 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) {
NodeName: "edge-01", NodeName: "edge-01",
NodeIP: "10.0.0.8", NodeIP: "10.0.0.8",
AgentVersion: config.AgentVersion, AgentVersion: config.AgentVersion,
NginxVersion: "1.25.5", NginxVersion: "1.27.1.2",
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
SyncInterval: config.MillisecondDuration(20 * time.Millisecond), SyncInterval: config.MillisecondDuration(20 * time.Millisecond),
}, },
@@ -126,7 +126,7 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) {
if loadErr != nil { if loadErr != nil {
t.Fatalf("failed to load state: %v", loadErr) t.Fatalf("failed to load state: %v", loadErr)
} }
if snapshot.LastError != "当前没有激活版本,保持当前 Nginx 配置" { if snapshot.LastError != "当前没有激活版本,保持当前 OpenResty 配置" {
t.Fatalf("expected startup sync error to be recorded, got %q", snapshot.LastError) t.Fatalf("expected startup sync error to be recorded, got %q", snapshot.LastError)
} }
} }
@@ -141,7 +141,7 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) {
heartbeatErrs: []error{errors.New("heartbeat timeout")}, heartbeatErrs: []error{errors.New("heartbeat timeout")},
} }
syncService := &fakeSyncService{ syncService := &fakeSyncService{
syncOnceErr: errors.New("nginx reload failed"), syncOnceErr: errors.New("openresty reload failed"),
onSyncOnceCall: func(callCount int) { onSyncOnceCall: func(callCount int) {
if callCount >= 1 { if callCount >= 1 {
cancel() cancel()
@@ -154,7 +154,7 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) {
NodeName: "edge-01", NodeName: "edge-01",
NodeIP: "10.0.0.8", NodeIP: "10.0.0.8",
AgentVersion: config.AgentVersion, AgentVersion: config.AgentVersion,
NginxVersion: "1.25.5", NginxVersion: "1.27.1.2",
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
SyncInterval: config.MillisecondDuration(10 * time.Millisecond), SyncInterval: config.MillisecondDuration(10 * time.Millisecond),
}, },
@@ -177,7 +177,7 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) {
if loadErr != nil { if loadErr != nil {
t.Fatalf("failed to load state: %v", loadErr) t.Fatalf("failed to load state: %v", loadErr)
} }
if snapshot.LastError != "nginx reload failed" { if snapshot.LastError != "openresty reload failed" {
t.Fatalf("expected sync error to be recorded, got %q", snapshot.LastError) t.Fatalf("expected sync error to be recorded, got %q", snapshot.LastError)
} }
} }
@@ -215,7 +215,7 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) {
NodeName: cfg.NodeName, NodeName: cfg.NodeName,
NodeIP: cfg.NodeIP, NodeIP: cfg.NodeIP,
AgentVersion: config.AgentVersion, AgentVersion: config.AgentVersion,
NginxVersion: "1.25.5", NginxVersion: "1.27.1.2",
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
SyncInterval: config.MillisecondDuration(20 * time.Millisecond), SyncInterval: config.MillisecondDuration(20 * time.Millisecond),
}, },
@@ -225,7 +225,7 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) {
} }
runner.Config = cfg runner.Config = cfg
runner.Config.AgentVersion = config.AgentVersion runner.Config.AgentVersion = config.AgentVersion
runner.Config.NginxVersion = "1.25.5" runner.Config.NginxVersion = "1.27.1.2"
runner.Config.HeartbeatInterval = config.MillisecondDuration(10 * time.Millisecond) runner.Config.HeartbeatInterval = config.MillisecondDuration(10 * time.Millisecond)
runner.Config.SyncInterval = config.MillisecondDuration(20 * time.Millisecond) runner.Config.SyncInterval = config.MillisecondDuration(20 * time.Millisecond)
+55 -16
View File
@@ -15,7 +15,7 @@ const (
defaultDockerRouteConfigRelativePath = "etc/nginx/conf.d/atsflare_routes.conf" defaultDockerRouteConfigRelativePath = "etc/nginx/conf.d/atsflare_routes.conf"
defaultCertDirRelativePath = "etc/nginx/certs" defaultCertDirRelativePath = "etc/nginx/certs"
defaultDockerStateRelativePath = "var/lib/atsflare/agent-state.json" defaultDockerStateRelativePath = "var/lib/atsflare/agent-state.json"
defaultDockerNginxCertDir = "/etc/nginx/atsflare-certs" defaultDockerOpenRestyCertDir = "/etc/nginx/atsflare-certs"
) )
type Config struct { type Config struct {
@@ -26,14 +26,14 @@ type Config struct {
NodeIP string `json:"node_ip"` NodeIP string `json:"node_ip"`
AgentVersion string `json:"-"` AgentVersion string `json:"-"`
NginxVersion string `json:"-"` NginxVersion string `json:"-"`
NginxPath string `json:"nginx_path"` OpenrestyPath string `json:"openresty_path"`
NginxContainerName string `json:"nginx_container_name"` OpenrestyContainerName string `json:"openresty_container_name"`
NginxDockerImage string `json:"nginx_docker_image"` OpenrestyDockerImage string `json:"openresty_docker_image"`
DockerBinary string `json:"docker_binary"` DockerBinary string `json:"docker_binary"`
DataDir string `json:"data_dir"` DataDir string `json:"data_dir"`
RouteConfigPath string `json:"route_config_path"` RouteConfigPath string `json:"route_config_path"`
CertDir string `json:"cert_dir"` CertDir string `json:"cert_dir"`
NginxCertDir string `json:"nginx_cert_dir"` OpenrestyCertDir string `json:"openresty_cert_dir"`
StatePath string `json:"state_path"` StatePath string `json:"state_path"`
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"` HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
SyncInterval MillisecondDuration `json:"sync_interval"` SyncInterval MillisecondDuration `json:"sync_interval"`
@@ -41,15 +41,54 @@ type Config struct {
configPath string configPath string
} }
type configFile 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"`
OpenrestyPath string `json:"openresty_path"`
OpenrestyContainerName string `json:"openresty_container_name"`
OpenrestyDockerImage string `json:"openresty_docker_image"`
DockerBinary string `json:"docker_binary"`
DataDir string `json:"data_dir"`
RouteConfigPath string `json:"route_config_path"`
CertDir string `json:"cert_dir"`
OpenrestyCertDir string `json:"openresty_cert_dir"`
StatePath string `json:"state_path"`
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
SyncInterval MillisecondDuration `json:"sync_interval"`
RequestTimeout MillisecondDuration `json:"request_timeout"`
}
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg := &Config{} file := &configFile{}
if err = json.Unmarshal(data, cfg); err != nil { if err = json.Unmarshal(data, file); err != nil {
return nil, err return nil, err
} }
cfg := &Config{
ServerURL: file.ServerURL,
AgentToken: file.AgentToken,
DiscoveryToken: file.DiscoveryToken,
NodeName: file.NodeName,
NodeIP: file.NodeIP,
OpenrestyPath: file.OpenrestyPath,
OpenrestyContainerName: file.OpenrestyContainerName,
OpenrestyDockerImage: file.OpenrestyDockerImage,
DockerBinary: file.DockerBinary,
DataDir: file.DataDir,
RouteConfigPath: file.RouteConfigPath,
CertDir: file.CertDir,
OpenrestyCertDir: file.OpenrestyCertDir,
StatePath: file.StatePath,
HeartbeatInterval: file.HeartbeatInterval,
SyncInterval: file.SyncInterval,
RequestTimeout: file.RequestTimeout,
}
cfg.configPath = path cfg.configPath = path
applyDefaults(cfg, filepath.Dir(path)) applyDefaults(cfg, filepath.Dir(path))
if err = validate(cfg); err != nil { if err = validate(cfg); err != nil {
@@ -61,11 +100,11 @@ func Load(path string) (*Config, error) {
func applyDefaults(cfg *Config, baseDir string) { func applyDefaults(cfg *Config, baseDir string) {
baseDir = filepath.Clean(baseDir) baseDir = filepath.Clean(baseDir)
cfg.AgentVersion = AgentVersion cfg.AgentVersion = AgentVersion
if cfg.NginxContainerName == "" { if cfg.OpenrestyContainerName == "" {
cfg.NginxContainerName = "atsflare-nginx" cfg.OpenrestyContainerName = "atsflare-openresty"
} }
if cfg.NginxDockerImage == "" { if cfg.OpenrestyDockerImage == "" {
cfg.NginxDockerImage = "nginx:stable-alpine" cfg.OpenrestyDockerImage = "openresty/openresty:alpine"
} }
if cfg.DockerBinary == "" { if cfg.DockerBinary == "" {
cfg.DockerBinary = "docker" cfg.DockerBinary = "docker"
@@ -79,7 +118,7 @@ func applyDefaults(cfg *Config, baseDir string) {
if cfg.NodeIP == "" { if cfg.NodeIP == "" {
cfg.NodeIP = detectNodeIP() cfg.NodeIP = detectNodeIP()
} }
if cfg.NginxPath == "" { if cfg.OpenrestyPath == "" {
cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultDockerRouteConfigRelativePath) cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultDockerRouteConfigRelativePath)
cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath) cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath)
} else { } else {
@@ -93,11 +132,11 @@ func applyDefaults(cfg *Config, baseDir string) {
if cfg.CertDir == "" { if cfg.CertDir == "" {
cfg.CertDir = joinManagedPath(cfg.DataDir, defaultCertDirRelativePath) cfg.CertDir = joinManagedPath(cfg.DataDir, defaultCertDirRelativePath)
} }
if cfg.NginxCertDir == "" { if cfg.OpenrestyCertDir == "" {
if cfg.NginxPath != "" { if cfg.OpenrestyPath != "" {
cfg.NginxCertDir = cfg.CertDir cfg.OpenrestyCertDir = cfg.CertDir
} else { } else {
cfg.NginxCertDir = defaultDockerNginxCertDir cfg.OpenrestyCertDir = defaultDockerOpenRestyCertDir
} }
} }
if cfg.HeartbeatInterval <= 0 { if cfg.HeartbeatInterval <= 0 {
+15 -6
View File
@@ -39,8 +39,14 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) {
if cfg.CertDir != filepath.Join(dir, "data", defaultCertDirRelativePath) { if cfg.CertDir != filepath.Join(dir, "data", defaultCertDirRelativePath) {
t.Fatalf("unexpected cert dir: %s", cfg.CertDir) t.Fatalf("unexpected cert dir: %s", cfg.CertDir)
} }
if cfg.NginxCertDir != defaultDockerNginxCertDir { if cfg.OpenrestyContainerName != "atsflare-openresty" {
t.Fatalf("unexpected nginx cert dir: %s", cfg.NginxCertDir) t.Fatalf("unexpected openresty container name: %s", cfg.OpenrestyContainerName)
}
if cfg.OpenrestyDockerImage != "openresty/openresty:alpine" {
t.Fatalf("unexpected openresty image: %s", cfg.OpenrestyDockerImage)
}
if cfg.OpenrestyCertDir != defaultDockerOpenRestyCertDir {
t.Fatalf("unexpected openresty cert dir: %s", cfg.OpenrestyCertDir)
} }
if cfg.StatePath != filepath.Join(dir, "data", defaultDockerStateRelativePath) { if cfg.StatePath != filepath.Join(dir, "data", defaultDockerStateRelativePath) {
t.Fatalf("unexpected state path: %s", cfg.StatePath) t.Fatalf("unexpected state path: %s", cfg.StatePath)
@@ -55,7 +61,7 @@ func TestLoadPathModeKeepsExplicitPaths(t *testing.T) {
"agent_token": "token", "agent_token": "token",
"node_name": "edge-01", "node_name": "edge-01",
"node_ip": "10.0.0.8", "node_ip": "10.0.0.8",
"nginx_path": "/opt/nginx/sbin/nginx", "openresty_path": "/usr/local/openresty/nginx/sbin/openresty",
"route_config_path": "/tmp/routes.conf", "route_config_path": "/tmp/routes.conf",
"state_path": "/tmp/agent-state.json", "state_path": "/tmp/agent-state.json",
} }
@@ -78,8 +84,8 @@ func TestLoadPathModeKeepsExplicitPaths(t *testing.T) {
if cfg.StatePath != "/tmp/agent-state.json" { if cfg.StatePath != "/tmp/agent-state.json" {
t.Fatalf("unexpected state path: %s", cfg.StatePath) t.Fatalf("unexpected state path: %s", cfg.StatePath)
} }
if cfg.NginxCertDir != cfg.CertDir { if cfg.OpenrestyCertDir != cfg.CertDir {
t.Fatalf("expected path mode nginx cert dir to equal cert dir, got %s / %s", cfg.NginxCertDir, cfg.CertDir) t.Fatalf("expected path mode openresty cert dir to equal cert dir, got %s / %s", cfg.OpenrestyCertDir, cfg.CertDir)
} }
} }
@@ -164,7 +170,7 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Load failed: %v", err) t.Fatalf("Load failed: %v", err)
} }
cfg.NginxVersion = "1.25.5" cfg.NginxVersion = "1.27.1.2"
cfg.HeartbeatInterval = MillisecondDuration(5 * time.Second) cfg.HeartbeatInterval = MillisecondDuration(5 * time.Second)
cfg.SyncInterval = MillisecondDuration(6 * time.Second) cfg.SyncInterval = MillisecondDuration(6 * time.Second)
cfg.RequestTimeout = MillisecondDuration(7 * time.Second) cfg.RequestTimeout = MillisecondDuration(7 * time.Second)
@@ -196,6 +202,9 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) {
if decoded["request_timeout"] != float64(7000) { if decoded["request_timeout"] != float64(7000) {
t.Fatalf("unexpected request timeout: %#v", decoded["request_timeout"]) t.Fatalf("unexpected request timeout: %#v", decoded["request_timeout"])
} }
if _, ok := decoded["nginx_path"]; ok {
t.Fatal("legacy nginx_path should not be persisted")
}
} }
func TestInitialAuthToken(t *testing.T) { func TestInitialAuthToken(t *testing.T) {
+58 -45
View File
@@ -19,6 +19,8 @@ import (
const CertDirPlaceholder = "__ATSF_CERT_DIR__" const CertDirPlaceholder = "__ATSF_CERT_DIR__"
const dockerRuntimeCommand = "openresty"
type Executor interface { type Executor interface {
Test(ctx context.Context) error Test(ctx context.Context) error
Reload(ctx context.Context) error Reload(ctx context.Context) error
@@ -43,22 +45,22 @@ type PathExecutor struct {
} }
func (e *PathExecutor) Test(ctx context.Context) error { func (e *PathExecutor) Test(ctx context.Context) error {
log.Printf("running nginx test with binary: %s", e.Path) log.Printf("running openresty test with binary: %s", e.Path)
output, err := e.Runner.Run(ctx, e.Path, "-t") output, err := e.Runner.Run(ctx, e.Path, "-t")
if err != nil { if err != nil {
return fmt.Errorf("nginx -t failed: %w: %s", err, string(output)) return fmt.Errorf("openresty -t failed: %w: %s", err, string(output))
} }
log.Printf("nginx test succeeded with binary: %s", e.Path) log.Printf("openresty test succeeded with binary: %s", e.Path)
return nil return nil
} }
func (e *PathExecutor) Reload(ctx context.Context) error { func (e *PathExecutor) Reload(ctx context.Context) error {
log.Printf("running nginx reload with binary: %s", e.Path) log.Printf("running openresty reload with binary: %s", e.Path)
output, err := e.Runner.Run(ctx, e.Path, "-s", "reload") output, err := e.Runner.Run(ctx, e.Path, "-s", "reload")
if err != nil { if err != nil {
return fmt.Errorf("nginx reload failed: %w: %s", err, string(output)) return fmt.Errorf("openresty reload failed: %w: %s", err, string(output))
} }
log.Printf("nginx reload succeeded with binary: %s", e.Path) log.Printf("openresty reload succeeded with binary: %s", e.Path)
return nil return nil
} }
@@ -77,24 +79,12 @@ type DockerExecutor struct {
} }
func (e *DockerExecutor) Test(ctx context.Context) error { func (e *DockerExecutor) Test(ctx context.Context) error {
log.Printf("running docker nginx test: container=%s image=%s", e.ContainerName, e.Image) log.Printf("running docker openresty test: container=%s image=%s", e.ContainerName, e.Image)
output, err := e.Runner.Run( output, err := e.runEphemeralRuntimeCommand(ctx, "-t")
ctx,
e.DockerBinary,
"run",
"--rm",
"-v",
fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir),
"-v",
fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir),
e.Image,
"nginx",
"-t",
)
if err != nil { if err != nil {
return fmt.Errorf("docker nginx -t failed: %w: %s", err, string(output)) return fmt.Errorf("docker %s -t failed: %w: %s", dockerRuntimeCommand, err, string(output))
} }
log.Printf("docker nginx test succeeded: container=%s", e.ContainerName) log.Printf("docker openresty test succeeded: container=%s runtime=%s", e.ContainerName, dockerRuntimeCommand)
return nil return nil
} }
@@ -103,7 +93,7 @@ func (e *DockerExecutor) Reload(ctx context.Context) error {
} }
func (e *DockerExecutor) EnsureRuntime(ctx context.Context, recreate bool) error { func (e *DockerExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
log.Printf("ensuring docker nginx runtime: container=%s recreate=%t", e.ContainerName, recreate) log.Printf("ensuring docker openresty runtime: container=%s recreate=%t", e.ContainerName, recreate)
output, err := e.Runner.Run(ctx, e.DockerBinary, "inspect", "-f", "{{.State.Running}}", e.ContainerName) output, err := e.Runner.Run(ctx, e.DockerBinary, "inspect", "-f", "{{.State.Running}}", e.ContainerName)
if err == nil { if err == nil {
if recreate { if recreate {
@@ -113,7 +103,7 @@ func (e *DockerExecutor) EnsureRuntime(ctx context.Context, recreate bool) error
return e.runContainer(ctx) return e.runContainer(ctx)
} }
if strings.TrimSpace(string(output)) == "true" { if strings.TrimSpace(string(output)) == "true" {
log.Printf("docker nginx runtime already healthy: container=%s", e.ContainerName) log.Printf("docker openresty runtime already healthy: container=%s", e.ContainerName)
return nil return nil
} }
if err := e.removeContainer(ctx); err != nil { if err := e.removeContainer(ctx); err != nil {
@@ -125,21 +115,21 @@ func (e *DockerExecutor) EnsureRuntime(ctx context.Context, recreate bool) error
} }
func (e *DockerExecutor) removeContainer(ctx context.Context) error { func (e *DockerExecutor) removeContainer(ctx context.Context) error {
log.Printf("removing docker nginx container: container=%s", e.ContainerName) log.Printf("removing docker openresty container: container=%s", e.ContainerName)
output, err := e.Runner.Run(ctx, e.DockerBinary, "rm", "-f", e.ContainerName) output, err := e.Runner.Run(ctx, e.DockerBinary, "rm", "-f", e.ContainerName)
if err != nil { if err != nil {
text := string(output) text := string(output)
if strings.Contains(text, "No such container") { if strings.Contains(text, "No such container") {
return nil return nil
} }
return fmt.Errorf("docker rm nginx failed: %w: %s", err, text) return fmt.Errorf("docker rm openresty failed: %w: %s", err, text)
} }
log.Printf("docker nginx container removed: container=%s", e.ContainerName) log.Printf("docker openresty container removed: container=%s", e.ContainerName)
return nil return nil
} }
func (e *DockerExecutor) runContainer(ctx context.Context) error { func (e *DockerExecutor) runContainer(ctx context.Context) error {
log.Printf("starting docker nginx container: container=%s image=%s", e.ContainerName, e.Image) log.Printf("starting docker openresty container: container=%s image=%s", e.ContainerName, e.Image)
runArgs := []string{ runArgs := []string{
"run", "-d", "run", "-d",
"--name", e.ContainerName, "--name", e.ContainerName,
@@ -151,9 +141,9 @@ func (e *DockerExecutor) runContainer(ctx context.Context) error {
} }
runOutput, runErr := e.Runner.Run(ctx, e.DockerBinary, runArgs...) runOutput, runErr := e.Runner.Run(ctx, e.DockerBinary, runArgs...)
if runErr != nil { if runErr != nil {
return fmt.Errorf("docker run nginx failed: %w: %s", runErr, string(runOutput)) return fmt.Errorf("docker run openresty failed: %w: %s", runErr, string(runOutput))
} }
log.Printf("docker nginx container started: container=%s", e.ContainerName) log.Printf("docker openresty container started: container=%s", e.ContainerName)
return nil return nil
} }
@@ -165,7 +155,7 @@ type Manager struct {
} }
func (m *Manager) Apply(ctx context.Context, content string, supportFiles []protocol.SupportFile) error { func (m *Manager) Apply(ctx context.Context, content string, supportFiles []protocol.SupportFile) error {
log.Printf("nginx apply started: route_config=%s support_files=%d", m.RouteConfigPath, len(supportFiles)) log.Printf("openresty apply started: route_config=%s support_files=%d", m.RouteConfigPath, len(supportFiles))
backup, err := m.backup() backup, err := m.backup()
if err != nil { if err != nil {
return err return err
@@ -177,21 +167,21 @@ func (m *Manager) Apply(ctx context.Context, content string, supportFiles []prot
} }
renderedContent := m.renderConfig(content) renderedContent := m.renderConfig(content)
if err = os.WriteFile(m.RouteConfigPath, []byte(renderedContent), 0o644); err != nil { if err = os.WriteFile(m.RouteConfigPath, []byte(renderedContent), 0o644); err != nil {
log.Printf("writing nginx route config failed, restoring backup: error=%v", err) log.Printf("writing openresty route config failed, restoring backup: error=%v", err)
_ = m.restore(backup) _ = m.restore(backup)
return err return err
} }
if err = m.Executor.Test(ctx); err != nil { if err = m.Executor.Test(ctx); err != nil {
log.Printf("nginx test failed after config write, restoring backup: error=%v", err) log.Printf("openresty test failed after config write, restoring backup: error=%v", err)
_ = m.restore(backup) _ = m.restore(backup)
return err return err
} }
if err = m.Executor.Reload(ctx); err != nil { if err = m.Executor.Reload(ctx); err != nil {
log.Printf("nginx reload failed after config write, restoring backup: error=%v", err) log.Printf("openresty reload failed after config write, restoring backup: error=%v", err)
_ = m.restore(backup) _ = m.restore(backup)
return err return err
} }
log.Printf("nginx apply completed successfully: route_config=%s", m.RouteConfigPath) log.Printf("openresty apply completed successfully: route_config=%s", m.RouteConfigPath)
return nil return nil
} }
@@ -199,7 +189,7 @@ func (m *Manager) EnsureRuntime(ctx context.Context, recreate bool) error {
if m.Executor == nil { if m.Executor == nil {
return errors.New("executor 未配置") return errors.New("executor 未配置")
} }
log.Printf("nginx ensure runtime requested: recreate=%t", recreate) log.Printf("openresty ensure runtime requested: recreate=%t", recreate)
return m.Executor.EnsureRuntime(ctx, recreate) return m.Executor.EnsureRuntime(ctx, recreate)
} }
@@ -223,7 +213,7 @@ func (m *Manager) CurrentChecksum() (string, error) {
return "", err return "", err
} }
result := bundleChecksum(normalized, files) result := bundleChecksum(normalized, files)
log.Printf("nginx current checksum calculated: route_config=%s checksum=%s support_files=%d", m.RouteConfigPath, result, len(files)) log.Printf("openresty current checksum calculated: route_config=%s checksum=%s support_files=%d", m.RouteConfigPath, result, len(files))
return result, nil return result, nil
} }
@@ -267,10 +257,10 @@ func NewExecutor(options ExecutorOptions) Executor {
func DetectVersion(ctx context.Context, options ExecutorOptions) string { func DetectVersion(ctx context.Context, options ExecutorOptions) string {
version, err := detectVersion(ctx, options, &OSCommandRunner{}) version, err := detectVersion(ctx, options, &OSCommandRunner{})
if err != nil { if err != nil {
log.Printf("detect nginx version failed: %v", err) log.Printf("detect openresty version failed: %v", err)
return "" return ""
} }
log.Printf("detected nginx version: %s", version) log.Printf("detected openresty version: %s", version)
return version return version
} }
@@ -281,21 +271,21 @@ func detectVersion(ctx context.Context, options ExecutorOptions, runner CommandR
if options.NginxPath != "" { if options.NginxPath != "" {
output, err := runner.Run(ctx, options.NginxPath, "-v") output, err := runner.Run(ctx, options.NginxPath, "-v")
if err != nil { if err != nil {
return "", fmt.Errorf("run nginx -v failed: %w: %s", err, string(output)) return "", fmt.Errorf("run runtime -v failed: %w: %s", err, string(output))
} }
version := parseNginxVersion(string(output)) version := parseNginxVersion(string(output))
if version == "" { if version == "" {
return "", errors.New("cannot parse nginx version from binary output") return "", errors.New("cannot parse runtime version from binary output")
} }
return version, nil return version, nil
} }
output, err := runner.Run(ctx, options.DockerBinary, "run", "--rm", options.Image, "nginx", "-v") output, err := runDockerVersionProbe(ctx, runner, options.DockerBinary, options.Image)
if err != nil { if err != nil {
return "", fmt.Errorf("run docker nginx -v failed: %w: %s", err, string(output)) return "", fmt.Errorf("run docker %s -v failed: %w: %s", dockerRuntimeCommand, err, string(output))
} }
version := parseNginxVersion(string(output)) version := parseNginxVersion(string(output))
if version == "" { if version == "" {
return "", errors.New("cannot parse nginx version from docker output") return "", errors.New("cannot parse runtime version from docker output")
} }
return version, nil return version, nil
} }
@@ -308,7 +298,30 @@ func parseNginxVersion(output string) string {
return matches[1] return matches[1]
} }
var nginxVersionPattern = regexp.MustCompile(`(?im)nginx version:\s*nginx/([^\s]+)`) var nginxVersionPattern = regexp.MustCompile(`(?im)(?:nginx|openresty) version:\s*(?:nginx|openresty)/([^\s]+)`)
func (e *DockerExecutor) runEphemeralRuntimeCommand(ctx context.Context, args ...string) ([]byte, error) {
return e.runEphemeralRuntimeCommandWithBinary(ctx, dockerRuntimeCommand, args...)
}
func (e *DockerExecutor) runEphemeralRuntimeCommandWithBinary(ctx context.Context, runtimeBinary string, args ...string) ([]byte, error) {
runtimeArgs := []string{
"run",
"--rm",
"-v",
fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir),
"-v",
fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir),
e.Image,
runtimeBinary,
}
runtimeArgs = append(runtimeArgs, args...)
return e.Runner.Run(ctx, e.DockerBinary, runtimeArgs...)
}
func runDockerVersionProbe(ctx context.Context, runner CommandRunner, dockerBinary string, image string) ([]byte, error) {
return runner.Run(ctx, dockerBinary, "run", "--rm", image, dockerRuntimeCommand, "-v")
}
type backupState struct { type backupState struct {
RouteExisted bool RouteExisted bool
+25 -22
View File
@@ -50,7 +50,7 @@ func (e *fakeExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
func TestPathExecutorCommands(t *testing.T) { func TestPathExecutorCommands(t *testing.T) {
runner := &fakeRunner{} runner := &fakeRunner{}
executor := &PathExecutor{ executor := &PathExecutor{
Path: "/opt/nginx/sbin/nginx", Path: "/usr/local/openresty/nginx/sbin/openresty",
Runner: runner, Runner: runner,
} }
@@ -62,8 +62,8 @@ func TestPathExecutorCommands(t *testing.T) {
} }
expected := []runCall{ expected := []runCall{
{name: "/opt/nginx/sbin/nginx", args: []string{"-t"}}, {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-t"}},
{name: "/opt/nginx/sbin/nginx", args: []string{"-s", "reload"}}, {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload"}},
} }
if !reflect.DeepEqual(runner.calls, expected) { if !reflect.DeepEqual(runner.calls, expected) {
t.Fatalf("unexpected calls: %#v", runner.calls) t.Fatalf("unexpected calls: %#v", runner.calls)
@@ -72,7 +72,7 @@ func TestPathExecutorCommands(t *testing.T) {
func TestPathExecutorEnsureRuntimeNoop(t *testing.T) { func TestPathExecutorEnsureRuntimeNoop(t *testing.T) {
executor := &PathExecutor{ executor := &PathExecutor{
Path: "/opt/nginx/sbin/nginx", Path: "/usr/local/openresty/nginx/sbin/openresty",
Runner: &fakeRunner{}, Runner: &fakeRunner{},
} }
if err := executor.EnsureRuntime(context.Background(), true); err != nil { if err := executor.EnsureRuntime(context.Background(), true); err != nil {
@@ -91,8 +91,8 @@ func TestDockerExecutorStartsContainerWhenMissing(t *testing.T) {
} }
executor := &DockerExecutor{ executor := &DockerExecutor{
DockerBinary: "docker", DockerBinary: "docker",
ContainerName: "atsflare-nginx", ContainerName: "atsflare-openresty",
Image: "nginx:stable-alpine", Image: "openresty/openresty:alpine",
RouteConfigDir: filepath.Clean("/tmp/routes"), RouteConfigDir: filepath.Clean("/tmp/routes"),
CertDir: filepath.Clean("/tmp/certs"), CertDir: filepath.Clean("/tmp/certs"),
NginxCertDir: "/etc/nginx/atsflare-certs", NginxCertDir: "/etc/nginx/atsflare-certs",
@@ -109,6 +109,9 @@ func TestDockerExecutorStartsContainerWhenMissing(t *testing.T) {
if runner.calls[0].args[0] != "run" || runner.calls[0].args[1] != "--rm" { if runner.calls[0].args[0] != "run" || runner.calls[0].args[1] != "--rm" {
t.Fatalf("expected docker run --rm for test, got %#v", runner.calls[0]) t.Fatalf("expected docker run --rm for test, got %#v", runner.calls[0])
} }
if runner.calls[0].args[len(runner.calls[0].args)-2] != "openresty" {
t.Fatalf("expected docker test command to invoke openresty, got %#v", runner.calls[0])
}
} }
func TestDockerExecutorStartsStoppedContainer(t *testing.T) { func TestDockerExecutorStartsStoppedContainer(t *testing.T) {
@@ -122,8 +125,8 @@ func TestDockerExecutorStartsStoppedContainer(t *testing.T) {
} }
executor := &DockerExecutor{ executor := &DockerExecutor{
DockerBinary: "docker", DockerBinary: "docker",
ContainerName: "atsflare-nginx", ContainerName: "atsflare-openresty",
Image: "nginx:stable-alpine", Image: "openresty/openresty:alpine",
RouteConfigDir: filepath.Clean("/tmp/routes"), RouteConfigDir: filepath.Clean("/tmp/routes"),
CertDir: filepath.Clean("/tmp/certs"), CertDir: filepath.Clean("/tmp/certs"),
NginxCertDir: "/etc/nginx/atsflare-certs", NginxCertDir: "/etc/nginx/atsflare-certs",
@@ -159,8 +162,8 @@ func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) {
} }
executor := &DockerExecutor{ executor := &DockerExecutor{
DockerBinary: "docker", DockerBinary: "docker",
ContainerName: "atsflare-nginx", ContainerName: "atsflare-openresty",
Image: "nginx:stable-alpine", Image: "openresty/openresty:alpine",
RouteConfigDir: filepath.Clean("/tmp/routes"), RouteConfigDir: filepath.Clean("/tmp/routes"),
CertDir: filepath.Clean("/tmp/certs"), CertDir: filepath.Clean("/tmp/certs"),
NginxCertDir: "/etc/nginx/atsflare-certs", NginxCertDir: "/etc/nginx/atsflare-certs",
@@ -184,8 +187,8 @@ func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) {
func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) { func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) {
executor := NewExecutor(ExecutorOptions{ executor := NewExecutor(ExecutorOptions{
DockerBinary: "docker", DockerBinary: "docker",
ContainerName: "atsflare-nginx", ContainerName: "atsflare-openresty",
Image: "nginx:stable-alpine", Image: "openresty/openresty:alpine",
RouteConfigPath: "./data/etc/nginx/conf.d/atsflare_routes.conf", RouteConfigPath: "./data/etc/nginx/conf.d/atsflare_routes.conf",
CertDir: "./data/etc/nginx/certs", CertDir: "./data/etc/nginx/certs",
NginxCertDir: "/etc/nginx/atsflare-certs", NginxCertDir: "/etc/nginx/atsflare-certs",
@@ -205,16 +208,16 @@ func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) {
func TestDetectVersionFromBinary(t *testing.T) { func TestDetectVersionFromBinary(t *testing.T) {
version, err := detectVersion(context.Background(), ExecutorOptions{ version, err := detectVersion(context.Background(), ExecutorOptions{
NginxPath: "/opt/nginx/sbin/nginx", NginxPath: "/usr/local/openresty/nginx/sbin/openresty",
}, &fakeRunner{ }, &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) { runFn: func(name string, args ...string) ([]byte, error) {
return []byte("nginx version: nginx/1.25.5\n"), nil return []byte("nginx version: openresty/1.27.1.2\n"), nil
}, },
}) })
if err != nil { if err != nil {
t.Fatalf("detectVersion failed: %v", err) t.Fatalf("detectVersion failed: %v", err)
} }
if version != "1.25.5" { if version != "1.27.1.2" {
t.Fatalf("unexpected version: %s", version) t.Fatalf("unexpected version: %s", version)
} }
} }
@@ -222,23 +225,23 @@ func TestDetectVersionFromBinary(t *testing.T) {
func TestDetectVersionFromDockerImage(t *testing.T) { func TestDetectVersionFromDockerImage(t *testing.T) {
runner := &fakeRunner{ runner := &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) { runFn: func(name string, args ...string) ([]byte, error) {
return []byte("nginx version: nginx/1.27.4\n"), nil return []byte("nginx version: openresty/1.27.1.2\n"), nil
}, },
} }
version, err := detectVersion(context.Background(), ExecutorOptions{ version, err := detectVersion(context.Background(), ExecutorOptions{
DockerBinary: "docker", DockerBinary: "docker",
Image: "nginx:stable-alpine", Image: "openresty/openresty:alpine",
}, runner) }, runner)
if err != nil { if err != nil {
t.Fatalf("detectVersion failed: %v", err) t.Fatalf("detectVersion failed: %v", err)
} }
if version != "1.27.4" { if version != "1.27.1.2" {
t.Fatalf("unexpected version: %s", version) t.Fatalf("unexpected version: %s", version)
} }
if len(runner.calls) != 1 { if len(runner.calls) != 1 {
t.Fatalf("expected one command call, got %d", len(runner.calls)) t.Fatalf("expected one command call, got %d", len(runner.calls))
} }
expectedArgs := []string{"run", "--rm", "nginx:stable-alpine", "nginx", "-v"} expectedArgs := []string{"run", "--rm", "openresty/openresty:alpine", "openresty", "-v"}
if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) { if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) {
t.Fatalf("unexpected docker args: %#v", runner.calls[0].args) t.Fatalf("unexpected docker args: %#v", runner.calls[0].args)
} }
@@ -247,11 +250,11 @@ func TestDetectVersionFromDockerImage(t *testing.T) {
func TestParseNginxVersionIgnoresDockerEntrypointPaths(t *testing.T) { func TestParseNginxVersionIgnoresDockerEntrypointPaths(t *testing.T) {
output := strings.Join([]string{ output := strings.Join([]string{
"/docker-entrypoint.sh: /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)", "/docker-entrypoint.sh: /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)",
"nginx version: nginx/1.27.4", "nginx version: openresty/1.27.1.2",
}, "\n") }, "\n")
version := parseNginxVersion(output) version := parseNginxVersion(output)
if version != "1.27.4" { if version != "1.27.1.2" {
t.Fatalf("unexpected version: %s", version) t.Fatalf("unexpected version: %s", version)
} }
} }
@@ -307,7 +310,7 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) {
CertDir: certDir, CertDir: certDir,
NginxCertDir: "/etc/nginx/atsflare-certs", NginxCertDir: "/etc/nginx/atsflare-certs",
Executor: &fakeExecutor{ Executor: &fakeExecutor{
testErr: errors.New("nginx test failed"), testErr: errors.New("openresty test failed"),
}, },
} }
+6 -6
View File
@@ -68,13 +68,13 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
} }
log.Printf("current local checksum loaded: mode=%s checksum=%s", mode, currentChecksum) log.Printf("current local checksum loaded: mode=%s checksum=%s", mode, currentChecksum)
if currentChecksum == config.Checksum { if currentChecksum == config.Checksum {
log.Printf("local nginx config already up to date: mode=%s version=%s", mode, config.Version) log.Printf("local openresty config already up to date: mode=%s version=%s", mode, config.Version)
if startup { if startup {
log.Printf("ensuring nginx runtime on startup: version=%s", config.Version) log.Printf("ensuring openresty runtime on startup: version=%s", config.Version)
if err = s.nginxManager.EnsureRuntime(ctx, true); err != nil { if err = s.nginxManager.EnsureRuntime(ctx, true); err != nil {
return err return err
} }
log.Printf("nginx runtime ensured on startup: version=%s", config.Version) log.Printf("openresty runtime ensured on startup: version=%s", config.Version)
} }
snapshot.CurrentVersion = config.Version snapshot.CurrentVersion = config.Version
snapshot.CurrentChecksum = config.Checksum snapshot.CurrentChecksum = config.Checksum
@@ -86,9 +86,9 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
log.Printf("skipping apply because state already records target version/checksum: version=%s checksum=%s", config.Version, config.Checksum) log.Printf("skipping apply because state already records target version/checksum: version=%s checksum=%s", config.Version, config.Checksum)
return nil return nil
} }
log.Printf("applying new nginx config: mode=%s from_version=%s to_version=%s old_checksum=%s new_checksum=%s", mode, snapshot.CurrentVersion, config.Version, currentChecksum, config.Checksum) log.Printf("applying new openresty config: mode=%s from_version=%s to_version=%s old_checksum=%s new_checksum=%s", mode, snapshot.CurrentVersion, config.Version, currentChecksum, config.Checksum)
if err = s.nginxManager.Apply(ctx, config.RenderedConfig, config.SupportFiles); err != nil { if err = s.nginxManager.Apply(ctx, config.RenderedConfig, config.SupportFiles); err != nil {
log.Printf("apply nginx config failed: mode=%s version=%s error=%v", mode, config.Version, err) log.Printf("apply openresty config failed: mode=%s version=%s error=%v", mode, config.Version, err)
snapshot.LastError = err.Error() snapshot.LastError = err.Error()
_ = s.stateStore.Save(snapshot) _ = s.stateStore.Save(snapshot)
reportErr := s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{ reportErr := s.client.ReportApplyLog(ctx, protocol.ApplyLogPayload{
@@ -104,7 +104,7 @@ func (s *Service) sync(ctx context.Context, startup bool) error {
log.Printf("failed apply log reported: version=%s", config.Version) log.Printf("failed apply log reported: version=%s", config.Version)
return err return err
} }
log.Printf("nginx config applied successfully: mode=%s version=%s", mode, config.Version) log.Printf("openresty config applied successfully: mode=%s version=%s", mode, config.Version)
snapshot.CurrentVersion = config.Version snapshot.CurrentVersion = config.Version
snapshot.CurrentChecksum = config.Checksum snapshot.CurrentChecksum = config.Checksum
snapshot.LastError = "" snapshot.LastError = ""
+1 -1
View File
@@ -20,7 +20,7 @@ var (
var UploadPath = "upload" var UploadPath = "upload"
func printHelp() { func printHelp() {
fmt.Println("ATSFlare " + Version + " - Internal Nginx Control Plane.") fmt.Println("ATSFlare " + Version + " - Internal OpenResty Control Plane.")
fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.") fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.")
fmt.Println("GitHub: https://github.com/Rain-kl/ATSFlare") fmt.Println("GitHub: https://github.com/Rain-kl/ATSFlare")
fmt.Println("Usage: atsflare [--port <port>] [--log-dir <log directory>] [--version] [--help]") fmt.Println("Usage: atsflare [--port <port>] [--log-dir <log directory>] [--version] [--help]")
+4 -4
View File
@@ -190,7 +190,7 @@ func TestPhase2AgentLifecycle(t *testing.T) {
"name": "shanghai-edge-1", "name": "shanghai-edge-1",
"ip": "10.0.0.9", "ip": "10.0.0.9",
"agent_version": "0.1.1", "agent_version": "0.1.1",
"nginx_version": "1.25.5", "nginx_version": "1.27.1.2",
"current_version": "", "current_version": "",
"last_error": "", "last_error": "",
} }
@@ -224,7 +224,7 @@ func TestPhase2AgentLifecycle(t *testing.T) {
"node_id": "spoofed-node-id", "node_id": "spoofed-node-id",
"version": activeConfig.Version, "version": activeConfig.Version,
"result": service.ApplyResultFailed, "result": service.ApplyResultFailed,
"message": "nginx reload failed", "message": "openresty reload failed",
}) })
var failedApplyLog model.ApplyLog var failedApplyLog model.ApplyLog
decodeResponseData(t, failedApplyResp, &failedApplyLog) decodeResponseData(t, failedApplyResp, &failedApplyLog)
@@ -244,13 +244,13 @@ func TestPhase2AgentLifecycle(t *testing.T) {
if nodes[0].AgentToken != createdNode.AgentToken { if nodes[0].AgentToken != createdNode.AgentToken {
t.Fatal("expected node auth token to remain stable after occupancy") t.Fatal("expected node auth token to remain stable after occupancy")
} }
if nodes[0].LatestApplyResult != service.ApplyResultFailed || nodes[0].LatestApplyMessage != "nginx reload failed" { if nodes[0].LatestApplyResult != service.ApplyResultFailed || nodes[0].LatestApplyMessage != "openresty reload failed" {
t.Fatal("expected node list to expose latest apply status") t.Fatal("expected node list to expose latest apply status")
} }
if nodes[0].CurrentVersion != activeConfig.Version { if nodes[0].CurrentVersion != activeConfig.Version {
t.Fatal("expected node current_version to remain at last successful version") t.Fatal("expected node current_version to remain at last successful version")
} }
if nodes[0].LastError != "nginx reload failed" { if nodes[0].LastError != "openresty reload failed" {
t.Fatal("expected node last_error to reflect failed apply") t.Fatal("expected node last_error to reflect failed apply")
} }
+22 -21
View File
@@ -172,8 +172,8 @@ go run ./cmd/agent -config ./agent.json
"server_url": "http://127.0.0.1:3000", "server_url": "http://127.0.0.1:3000",
"discovery_token": "replace-with-global-discovery-token", "discovery_token": "replace-with-global-discovery-token",
"data_dir": "./data", "data_dir": "./data",
"nginx_container_name": "atsflare-nginx", "openresty_container_name": "atsflare-openresty",
"nginx_docker_image": "nginx:stable-alpine", "openresty_docker_image": "openresty/openresty:alpine",
"heartbeat_interval": 30000, "heartbeat_interval": 30000,
"sync_interval": 30000, "sync_interval": 30000,
"request_timeout": 10000 "request_timeout": 10000
@@ -189,10 +189,10 @@ go run ./cmd/agent -config ./agent.json
"node_name": "node-01", "node_name": "node-01",
"node_ip": "192.168.1.20", "node_ip": "192.168.1.20",
"data_dir": "./data", "data_dir": "./data",
"nginx_path": "/usr/sbin/nginx", "openresty_path": "/usr/local/openresty/nginx/sbin/openresty",
"route_config_path": "/etc/nginx/conf.d/atsflare_routes.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf",
"cert_dir": "/etc/nginx/certs", "cert_dir": "/usr/local/openresty/nginx/conf/certs",
"nginx_cert_dir": "/etc/nginx/certs", "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs",
"state_path": "./data/agent-state.json", "state_path": "./data/agent-state.json",
"heartbeat_interval": 30000, "heartbeat_interval": 30000,
"sync_interval": 30000, "sync_interval": 30000,
@@ -209,14 +209,14 @@ go run ./cmd/agent -config ./agent.json
| `discovery_token` | 全局发现 Token,用于节点首次自动注册 | 与 `agent_token` 二选一 | 空 | `discovery-token-xxx` | | `discovery_token` | 全局发现 Token,用于节点首次自动注册 | 与 `agent_token` 二选一 | 空 | `discovery-token-xxx` |
| `node_name` | 节点名称 | 否 | 自动使用主机名 | `node-01` | | `node_name` | 节点名称 | 否 | 自动使用主机名 | `node-01` |
| `node_ip` | 节点 IP | 否 | 自动探测第一个可用 IPv4 | `192.168.1.20` | | `node_ip` | 节点 IP | 否 | 自动探测第一个可用 IPv4 | `192.168.1.20` |
| `nginx_path` | 本机 Nginx 可执行文件路径;设置后按本机 Nginx 模式运行 | 否 | 空;未设置时按 Docker Nginx 模式处理 | `/usr/sbin/nginx` | | `openresty_path` | 本机 OpenResty 可执行文件路径;设置后按本机 OpenResty 模式运行 | 否 | 空;未设置时按 Docker OpenResty 模式处理 | `/usr/local/openresty/nginx/sbin/openresty` |
| `nginx_container_name` | Docker 模式下的 Nginx 容器名 | 否 | `atsflare-nginx` | `atsflare-nginx` | | `openresty_container_name` | Docker 模式下的 OpenResty 容器名 | 否 | `atsflare-openresty` | `atsflare-openresty` |
| `nginx_docker_image` | Docker 模式下用于初始化/管理的 Nginx 镜像 | 否 | `nginx:stable-alpine` | `nginx:stable-alpine` | | `openresty_docker_image` | Docker 模式下用于初始化/管理的 OpenResty 镜像 | 否 | `openresty/openresty:alpine` | `openresty/openresty:alpine` |
| `docker_binary` | Docker 可执行文件名或路径 | 否 | `docker` | `/usr/bin/docker` | | `docker_binary` | Docker 可执行文件名或路径 | 否 | `docker` | `/usr/bin/docker` |
| `data_dir` | Agent 数据目录,用于存储托管配置、证书和状态文件 | 否 | 配置文件所在目录下的 `data` 子目录 | `./data` | | `data_dir` | Agent 数据目录,用于存储托管配置、证书和状态文件 | 否 | 配置文件所在目录下的 `data` 子目录 | `./data` |
| `route_config_path` | 路由配置文件写入路径 | 否 | 默认为 `data_dir` 下托管路径 | `/etc/nginx/conf.d/atsflare_routes.conf` | | `route_config_path` | 路由配置文件写入路径 | 否 | 默认为 `data_dir` 下托管路径 | `/etc/nginx/conf.d/atsflare_routes.conf` |
| `cert_dir` | Agent 在本机写入证书文件的目录 | 否 | 默认为 `data_dir` 下托管证书目录 | `./data/etc/nginx/certs` | | `cert_dir` | Agent 在本机写入证书文件的目录 | 否 | 默认为 `data_dir` 下托管证书目录 | `./data/etc/nginx/certs` |
| `nginx_cert_dir` | Nginx 实际读取证书的目录 | 否 | 本机模式默认等于 `cert_dir`;Docker 模式默认 `/etc/nginx/atsflare-certs` | `/etc/nginx/certs` | | `openresty_cert_dir` | OpenResty 实际读取证书的目录 | 否 | 本机模式默认等于 `cert_dir`;Docker 模式默认 `/etc/nginx/atsflare-certs` | `/usr/local/openresty/nginx/conf/certs` |
| `state_path` | Agent 本地状态文件路径 | 否 | 默认为 `data_dir` 下托管状态文件 | `./data/agent-state.json` | | `state_path` | Agent 本地状态文件路径 | 否 | 默认为 `data_dir` 下托管状态文件 | `./data/agent-state.json` |
| `heartbeat_interval` | 心跳间隔 | 否 | `30000` 毫秒 | `30000` | | `heartbeat_interval` | 心跳间隔 | 否 | `30000` 毫秒 | `30000` |
| `sync_interval` | 配置同步间隔 | 否 | `30000` 毫秒 | `30000` | | `sync_interval` | 配置同步间隔 | 否 | `30000` 毫秒 | `30000` |
@@ -229,8 +229,9 @@ go run ./cmd/agent -config ./agent.json
* 毫秒整数,例如 `30000` * 毫秒整数,例如 `30000`
* Go duration 字符串,例如 `"30s"` * Go duration 字符串,例如 `"30s"`
* `node_name` 与 `node_ip` 未填写时会自动探测;若自动探测失败,配置校验会报错 * `node_name` 与 `node_ip` 未填写时会自动探测;若自动探测失败,配置校验会报错
* 未配置 `nginx_path` 时,默认为 Docker Nginx 模式 * 未配置 `openresty_path` 时,默认为 Docker OpenResty 模式
* 配置保存时,`agent_version`、`nginx_version` 由程序运行时维护,不需要写入 JSON * 配置保存时,`agent_version`、`nginx_version` 由程序运行时维护,不需要写入 JSON
* 本机模式下的 `route_config_path` 需与节点主配置文件的 include 规则保持一致
### 2.4 Agent 托管路径默认值 ### 2.4 Agent 托管路径默认值
@@ -242,17 +243,17 @@ go run ./cmd/agent -config ./agent.json
| `cert_dir` | `data_dir/etc/nginx/certs` | | `cert_dir` | `data_dir/etc/nginx/certs` |
| `state_path` | `data_dir/var/lib/atsflare/agent-state.json` | | `state_path` | `data_dir/var/lib/atsflare/agent-state.json` |
Docker Nginx 模式下: Docker OpenResty 模式下:
| 字段 | 默认值 | | 字段 | 默认值 |
| --- | --- | | --- | --- |
| `nginx_cert_dir` | `/etc/nginx/atsflare-certs` | | `openresty_cert_dir` | `/etc/nginx/atsflare-certs` |
### 2.5 Agent 启动示例 ### 2.5 Agent 启动示例
#### Docker Nginx 模式 #### Docker OpenResty 模式
适用于节点本机不直接管理宿主机 Nginx,而是通过 Docker 容器运行 Nginx。 适用于节点本机不直接管理宿主机 OpenResty,而是通过 Docker 容器运行 OpenResty。
```json ```json
{ {
@@ -262,18 +263,18 @@ Docker Nginx 模式下:
} }
``` ```
#### 本机 Nginx 模式 #### 本机 OpenResty 模式
适用于节点已经安装了宿主机 Nginx,且 Agent 直接执行 `nginx -t` 与 `nginx -s reload`。 适用于节点已经安装了宿主机 OpenResty,且 Agent 直接执行 `openresty -t` 与 `openresty -s reload`。
```json ```json
{ {
"server_url": "http://127.0.0.1:3000", "server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token", "agent_token": "replace-with-node-auth-token",
"nginx_path": "/usr/sbin/nginx", "openresty_path": "/usr/local/openresty/nginx/sbin/openresty",
"route_config_path": "/etc/nginx/conf.d/atsflare_routes.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf",
"cert_dir": "/etc/nginx/certs", "cert_dir": "/usr/local/openresty/nginx/conf/certs",
"nginx_cert_dir": "/etc/nginx/certs" "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs"
} }
``` ```
+27 -27
View File
@@ -16,7 +16,7 @@
* Go 1.18+ * Go 1.18+
* 对 Agent 数据目录有写权限 * 对 Agent 数据目录有写权限
* 若使用独立 Nginx 模式:可执行 `nginx -t` 与 `nginx -s reload` * 若使用独立 OpenResty 模式:可执行 `openresty -t` 与 `openresty -s reload`
* 若使用 Docker 模式:具备 Docker 执行权限 * 若使用 Docker 模式:具备 Docker 执行权限
--- ---
@@ -84,18 +84,18 @@ volumes:
docker compose up -d docker compose up -d
``` ```
说明: 说明:
* `SESSION_SECRET` 必须替换为随机字符串 * `SESSION_SECRET` 必须替换为随机字符串
* SQLite 数据文件持久化到 Docker volume `atsflare-data` * SQLite 数据文件持久化到 Docker volume `atsflare-data`
* 镜像默认监听容器内 `3000` 端口 * 镜像默认监听容器内 `3000` 端口
* 若要固定版本,可将 `latest` 替换为具体 tag,例如 `ghcr.io/rain-kl/atsflare:v0.3.0` * 若要固定版本,可将 `latest` 替换为具体 tag,例如 `ghcr.io/rain-kl/atsflare:v0.3.0`
版本升级说明: 版本升级说明:
* Root 用户可在管理端顶栏点击「版本」检查 GitHub 最新 Release * Root 用户可在管理端顶栏点击「版本」检查 GitHub 最新 Release
* 当前运行的是 Release 二进制且二进制目录可写时,可直接在弹窗内触发 Server 自升级 * 当前运行的是 Release 二进制且二进制目录可写时,可直接在弹窗内触发 Server 自升级
* 自升级会下载匹配当前平台的 `atsflare-server-*` 资产,替换当前二进制并自动重启进程 * 自升级会下载匹配当前平台的 `atsflare-server-*` 资产,替换当前二进制并自动重启进程
### 2.4 首次登录 ### 2.4 首次登录
@@ -146,8 +146,8 @@ swag init -g main.go -o docs
"server_url": "http://127.0.0.1:3000", "server_url": "http://127.0.0.1:3000",
"agent_token": "replace-with-node-auth-token", "agent_token": "replace-with-node-auth-token",
"data_dir": "./data", "data_dir": "./data",
"nginx_container_name": "atsflare-nginx", "openresty_container_name": "atsflare-openresty",
"nginx_docker_image": "nginx:stable-alpine", "openresty_docker_image": "openresty/openresty:alpine",
"heartbeat_interval": 30000, "heartbeat_interval": 30000,
"sync_interval": 30000, "sync_interval": 30000,
"request_timeout": 10000 "request_timeout": 10000
@@ -161,8 +161,8 @@ swag init -g main.go -o docs
"server_url": "http://127.0.0.1:3000", "server_url": "http://127.0.0.1:3000",
"discovery_token": "replace-with-global-discovery-token", "discovery_token": "replace-with-global-discovery-token",
"data_dir": "./data", "data_dir": "./data",
"nginx_container_name": "atsflare-nginx", "openresty_container_name": "atsflare-openresty",
"nginx_docker_image": "nginx:stable-alpine", "openresty_docker_image": "openresty/openresty:alpine",
"heartbeat_interval": 30000, "heartbeat_interval": 30000,
"sync_interval": 30000, "sync_interval": 30000,
"request_timeout": 10000 "request_timeout": 10000
@@ -172,12 +172,12 @@ swag init -g main.go -o docs
说明: 说明:
* `agent_version` 由 Agent 代码内常量提供,升级时同步修改代码 * `agent_version` 由 Agent 代码内常量提供,升级时同步修改代码
* `nginx_version` 由 Agent 启动时执行命令自动探测 * 为兼容现有 Agent / Server API,运行时版本仍通过 `openresty_version` 字段上报,但其值现在表示 OpenResty 版本
* 时间字段使用毫秒整数 * 时间字段使用毫秒整数
* `agent_token` 与 `discovery_token` 至少填写一个 * `agent_token` 与 `discovery_token` 至少填写一个
* 若 `agent_token` 为空且 `discovery_token` 存在,Agent 会自动注册并写回新的专属 `agent_token` * 若 `agent_token` 为空且 `discovery_token` 存在,Agent 会自动注册并写回新的专属 `agent_token`
* `node_name` 与 `node_ip` 可省略,未填写时自动探测 * `node_name` 与 `node_ip` 可省略,未填写时自动探测
* 未配置 `nginx_path` 时,默认使用 Docker Nginx 容器 * 未配置 `openresty_path` 时,默认使用 Docker OpenResty 容器
--- ---
@@ -223,8 +223,8 @@ go build -o atsflare-agent ./cmd/agent
2. 自动注册模式下完成 Token 置换 2. 自动注册模式下完成 Token 置换
3. 拉取激活版本 3. 拉取激活版本
4. 写入路由配置与必要证书文件 4. 写入路由配置与必要证书文件
5. 执行 `nginx -t` 5. 执行 `openresty -t`
6. 执行 `nginx -s reload` 6. 执行 `openresty -s reload`
7. 上报应用结果 7. 上报应用结果
### 5.4 验证管理端状态 ### 5.4 验证管理端状态
@@ -238,7 +238,7 @@ go build -o atsflare-agent ./cmd/agent
### 5.5 验证失败回滚 ### 5.5 验证失败回滚
人为制造 `nginx -t` 失败后再次发布,预期: 人为制造 `openresty -t` 失败后再次发布,预期:
* Agent 回滚旧配置 * Agent 回滚旧配置
* 节点 `last_error` 更新 * 节点 `last_error` 更新
@@ -276,11 +276,11 @@ pnpm build
* GitHub 使用 [.github/workflows/release.yml](.github/workflows/release.yml),保留制品上传/下载分阶段流程 * GitHub 使用 [.github/workflows/release.yml](.github/workflows/release.yml),保留制品上传/下载分阶段流程
* Gitea 使用 [.gitea/workflows/release.yml](.gitea/workflows/release.yml),在单个 Job 内完成前端构建、服务端/Agent 多平台编译与 Release 发布,避免依赖 Gitea 目前不兼容的 `upload-artifact@v4`、`download-artifact@v4` * Gitea 使用 [.gitea/workflows/release.yml](.gitea/workflows/release.yml),在单个 Job 内完成前端构建、服务端/Agent 多平台编译与 Release 发布,避免依赖 Gitea 目前不兼容的 `upload-artifact@v4`、`download-artifact@v4`
Docker 镜像发布使用 [.github/workflows/docker-image.yml](.github/workflows/docker-image.yml): Docker 镜像发布使用 [.github/workflows/docker-image.yml](.github/workflows/docker-image.yml):
* 仅构建 `atsf_server` 服务端镜像 * 仅构建 `atsf_server` 服务端镜像
* 发布到 GitHub Container Registry(`ghcr.io/<owner>/<repo>:<tag>`) * 发布到 GitHub Container Registry(`ghcr.io/<owner>/<repo>:<tag>`)
* 单个工作流通过分架构原生构建再合并 manifest 的方式产出 `linux/amd64` 与 `linux/arm64` 多架构镜像,避免 `arm64` 长时间模拟编译 * 单个工作流通过分架构原生构建再合并 manifest 的方式产出 `linux/amd64` 与 `linux/arm64` 多架构镜像,避免 `arm64` 长时间模拟编译
--- ---
+6 -6
View File
@@ -21,7 +21,7 @@ ATSFlare 当前定位为内部自用的反向代理控制面,不面向外部
* 反代规则管理 * 反代规则管理
* 配置预览、发布、激活与回滚 * 配置预览、发布、激活与回滚
* Agent 注册、心跳、同步、应用结果上报 * Agent 注册、心跳、同步、应用结果上报
* Nginx 配置写入、校验、reload 与失败回滚 * OpenResty 配置写入、校验、reload 与失败回滚
* HTTPS/TLS 路由支持 * HTTPS/TLS 路由支持
* 证书托管与域名管理 * 证书托管与域名管理
* 节点管理、节点专属 `agent_token`、全局 `discovery_token` * 节点管理、节点专属 `agent_token`、全局 `discovery_token`
@@ -74,8 +74,8 @@ ATSFlare 当前定位为内部自用的反向代理控制面,不面向外部
* 单二进制 * 单二进制
* 节点本地执行 * 节点本地执行
* `nginx_path` 优先 * `openresty_path` 优先
* 未配置 `nginx_path` 时默认使用 Docker Nginx * 未配置 `openresty_path` 时默认使用 Docker OpenResty
* 生成资源默认落在 `./data`,可由 `data_dir` 覆盖 * 生成资源默认落在 `./data`,可由 `data_dir` 覆盖
### 4.3 Frontend ### 4.3 Frontend
@@ -100,7 +100,7 @@ ATSFlare Server (Gin + SQLite + Web UI)
ATSFlare Agent (register / heartbeat / sync / apply / update) ATSFlare Agent (register / heartbeat / sync / apply / update)
| |
v v
Local Nginx or Docker Nginx Local OpenResty or Docker OpenResty
| |
v v
Origin Origin
@@ -150,7 +150,7 @@ ATSFlare Agent (register / heartbeat / sync / apply / update)
发布规则: 发布规则:
1. 读取全部启用的 `proxy_routes` 1. 读取全部启用的 `proxy_routes`
2. 渲染完整 Nginx 配置 2. 渲染完整 OpenResty 配置
3. 计算 `checksum` 3. 计算 `checksum`
4. 写入 `config_versions` 4. 写入 `config_versions`
5. 切换激活版本 5. 切换激活版本
@@ -185,7 +185,7 @@ ATSFlare Agent (register / heartbeat / sync / apply / update)
* 周期性心跳与同步 * 周期性心跳与同步
* 运行参数接收 * 运行参数接收
* 本地路由与证书文件写入 * 本地路由与证书文件写入
* `nginx -t` / `nginx -s reload` * 执行 `openresty -t` / `openresty -s reload`
* 失败回滚 * 失败回滚
* 自我更新 * 自我更新
* 应用结果上报 * 应用结果上报
+8 -8
View File
@@ -36,8 +36,8 @@
* 单二进制 * 单二进制
* 节点本地执行 * 节点本地执行
* `nginx_path` 优先 * `openresty_path` 优先
* 无 `nginx_path` 时默认 Docker Nginx * 无 `openresty_path` 时默认 Docker OpenResty
* 生成资源默认写入 `./data`,由 `data_dir` 统一覆盖 * 生成资源默认写入 `./data`,由 `data_dir` 统一覆盖
### 2.3 Frontend ### 2.3 Frontend
@@ -81,7 +81,7 @@
* `config` * `config`
* `heartbeat` * `heartbeat`
* `sync` * `sync`
* `nginx` * `openresty`(保留目录名,内部负责 OpenResty 运行时管理)
* `state` * `state`
* `httpclient` * `httpclient`
* `protocol` * `protocol`
@@ -170,7 +170,7 @@ Agent:
禁止: 禁止:
* 将本地 Nginx 操作暴露为远程执行接口 * 将本地 OpenResty 操作暴露为远程执行接口
* 在日志中打印完整 Token * 在日志中打印完整 Token
--- ---
@@ -180,7 +180,7 @@ Agent:
发布逻辑必须保持以下事实: 发布逻辑必须保持以下事实:
* 发布时读取全部启用的 `proxy_routes` * 发布时读取全部启用的 `proxy_routes`
* 生成完整 Nginx 配置 * 生成完整 OpenResty 配置
* 计算 `checksum` * 计算 `checksum`
* 写入 `config_versions` * 写入 `config_versions`
* 通过切换 `is_active` 激活版本 * 通过切换 `is_active` 激活版本
@@ -205,8 +205,8 @@ Agent 必须满足:
* 周期性心跳与同步 * 周期性心跳与同步
* 发现新版本时先备份旧文件 * 发现新版本时先备份旧文件
* 写入新路由与必要证书文件 * 写入新路由与必要证书文件
* 先执行 `nginx -t` * 先执行 `openresty -t`
* 成功后执行 `nginx -s reload` * 成功后执行 `openresty -s reload`
* 失败时自动回滚并上报最终结果 * 失败时自动回滚并上报最终结果
* 支持自动注册与 Token 置换 * 支持自动注册与 Token 置换
* 支持接收 Server 下发运行参数 * 支持接收 Server 下发运行参数
@@ -249,7 +249,7 @@ Agent 必须满足:
* Agent 注册 * Agent 注册
* 心跳异常 * 心跳异常
* 配置下载失败 * 配置下载失败
* Nginx 校验或 reload 成功/失败 * OpenResty 校验或 reload 成功/失败
* 回滚触发 * 回滚触发
要求: 要求: