diff --git a/README.md b/README.md index 91fa4255..cc0e8f9f 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,8 @@ Docker 镜像工作流仅构建 `atsf_server`,并产出 `linux/amd64` 与 `lin | `agent_token` | 节点专属认证 Token | | `discovery_token` | 首次自动注册使用的全局 Token | | `data_dir` | Agent 托管数据目录 | -| `support_dir` | Agent 存放受管附属文件的目录,当前包含证书与 Lua 观测脚本 | +| `cert_dir` | Agent 存放受管证书文件的目录 | +| `lua_dir` | Agent 存放受管 Lua 观测脚本的目录;启动时自动覆盖释放 | | `openresty_observability_port` | Agent 读取 OpenResty Lua 本地观测指标的 loopback 端口 | | `observability_buffer_path` | Agent 本地观测补报缓冲文件路径 | | `observability_replay_minutes` | Agent 恢复 heartbeat 后允许自动补传的最近观测窗口分钟数 | diff --git a/atsf_agent/cmd/agent/main.go b/atsf_agent/cmd/agent/main.go index 99437518..6689d39a 100644 --- a/atsf_agent/cmd/agent/main.go +++ b/atsf_agent/cmd/agent/main.go @@ -39,8 +39,10 @@ func main() { Image: cfg.OpenrestyDockerImage, MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, - SupportDir: cfg.SupportDir, - NginxSupportDir: cfg.OpenrestySupportDir, + CertDir: cfg.CertDir, + NginxCertDir: cfg.OpenrestyCertDir, + LuaDir: cfg.LuaDir, + NginxLuaDir: cfg.OpenrestyLuaDir, OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, }, ) @@ -50,7 +52,8 @@ func main() { "ip", cfg.NodeIP, "heartbeat_interval", cfg.HeartbeatInterval, "route_config", cfg.RouteConfigPath, - "support_dir", cfg.SupportDir, + "cert_dir", cfg.CertDir, + "lua_dir", cfg.LuaDir, ) client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration()) @@ -64,8 +67,10 @@ func main() { MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, RuntimeRouteConfigPath: runtimeRouteConfigPath, - SupportDir: cfg.SupportDir, - NginxSupportDir: cfg.OpenrestySupportDir, + CertDir: cfg.CertDir, + NginxCertDir: cfg.OpenrestyCertDir, + LuaDir: cfg.LuaDir, + NginxLuaDir: cfg.OpenrestyLuaDir, OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyPath, cfg.OpenrestyObservabilityPort), OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, Executor: nginx.NewExecutor(nginx.ExecutorOptions{ @@ -75,11 +80,17 @@ func main() { Image: cfg.OpenrestyDockerImage, MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, - SupportDir: cfg.SupportDir, - NginxSupportDir: cfg.OpenrestySupportDir, + CertDir: cfg.CertDir, + NginxCertDir: cfg.OpenrestyCertDir, + LuaDir: cfg.LuaDir, + NginxLuaDir: cfg.OpenrestyLuaDir, OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, }), } + if err = runtimeManager.EnsureLuaAssets(); err != nil { + slog.Error("ensure managed lua assets failed", "error", err) + os.Exit(1) + } runner := &agent.Runner{ Config: cfg, StateStore: stateStore, diff --git a/atsf_agent/internal/config/config.go b/atsf_agent/internal/config/config.go index 75e854d3..1eeba8a0 100644 --- a/atsf_agent/internal/config/config.go +++ b/atsf_agent/internal/config/config.go @@ -14,10 +14,12 @@ import ( const ( defaultDockerMainConfigRelativePath = "etc/nginx/nginx.conf" defaultDockerRouteConfigRelativePath = "etc/nginx/conf.d/atsflare_routes.conf" - defaultSupportDirRelativePath = "etc/nginx/support" + defaultCertDirRelativePath = "etc/nginx/certs" + defaultLuaDirRelativePath = "etc/nginx/lua" defaultDockerStateRelativePath = "var/lib/atsflare/agent-state.json" defaultObservabilityBufferRelativePath = "var/lib/atsflare/observability-buffer.json" - defaultDockerOpenRestySupportDir = "/etc/nginx/atsflare-support" + defaultDockerOpenRestyCertDir = "/etc/nginx/atsflare-certs" + defaultDockerOpenRestyLuaDir = "/etc/nginx/atsflare-lua" defaultOpenRestyObservabilityPort = 18081 defaultObservabilityReplayMinutes = 15 ) @@ -37,8 +39,10 @@ type Config struct { DataDir string `json:"data_dir"` MainConfigPath string `json:"main_config_path"` RouteConfigPath string `json:"route_config_path"` - SupportDir string `json:"support_dir"` - OpenrestySupportDir string `json:"openresty_support_dir"` + CertDir string `json:"cert_dir"` + OpenrestyCertDir string `json:"openresty_cert_dir"` + LuaDir string `json:"lua_dir"` + OpenrestyLuaDir string `json:"openresty_lua_dir"` OpenrestyObservabilityPort int `json:"openresty_observability_port"` ObservabilityBufferPath string `json:"observability_buffer_path"` ObservabilityReplayMinutes int `json:"observability_replay_minutes"` @@ -61,10 +65,10 @@ type configFile struct { DataDir string `json:"data_dir"` MainConfigPath string `json:"main_config_path"` RouteConfigPath string `json:"route_config_path"` - SupportDir string `json:"support_dir"` - OpenrestySupportDir string `json:"openresty_support_dir"` - LegacyCertDir string `json:"cert_dir"` - LegacyOpenrestyCertDir string `json:"openresty_cert_dir"` + CertDir string `json:"cert_dir"` + OpenrestyCertDir string `json:"openresty_cert_dir"` + LuaDir string `json:"lua_dir"` + OpenrestyLuaDir string `json:"openresty_lua_dir"` OpenrestyObservabilityPort int `json:"openresty_observability_port"` ObservabilityBufferPath string `json:"observability_buffer_path"` ObservabilityReplayMinutes int `json:"observability_replay_minutes"` @@ -95,8 +99,10 @@ func Load(path string) (*Config, error) { DataDir: file.DataDir, MainConfigPath: file.MainConfigPath, RouteConfigPath: file.RouteConfigPath, - SupportDir: firstNonEmpty(file.SupportDir, file.LegacyCertDir), - OpenrestySupportDir: firstNonEmpty(file.OpenrestySupportDir, file.LegacyOpenrestyCertDir), + CertDir: file.CertDir, + OpenrestyCertDir: file.OpenrestyCertDir, + LuaDir: file.LuaDir, + OpenrestyLuaDir: file.OpenrestyLuaDir, OpenrestyObservabilityPort: file.OpenrestyObservabilityPort, ObservabilityBufferPath: file.ObservabilityBufferPath, ObservabilityReplayMinutes: file.ObservabilityReplayMinutes, @@ -148,14 +154,24 @@ func applyDefaults(cfg *Config, baseDir string) { cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath) } } - if cfg.SupportDir == "" { - cfg.SupportDir = joinManagedPath(cfg.DataDir, defaultSupportDirRelativePath) + if cfg.CertDir == "" { + cfg.CertDir = joinManagedPath(cfg.DataDir, defaultCertDirRelativePath) } - if cfg.OpenrestySupportDir == "" { + if cfg.OpenrestyCertDir == "" { if cfg.OpenrestyPath != "" { - cfg.OpenrestySupportDir = cfg.SupportDir + cfg.OpenrestyCertDir = cfg.CertDir } else { - cfg.OpenrestySupportDir = defaultDockerOpenRestySupportDir + cfg.OpenrestyCertDir = defaultDockerOpenRestyCertDir + } + } + if cfg.LuaDir == "" { + cfg.LuaDir = joinManagedPath(cfg.DataDir, defaultLuaDirRelativePath) + } + if cfg.OpenrestyLuaDir == "" { + if cfg.OpenrestyPath != "" { + cfg.OpenrestyLuaDir = cfg.LuaDir + } else { + cfg.OpenrestyLuaDir = defaultDockerOpenRestyLuaDir } } if cfg.OpenrestyObservabilityPort <= 0 { @@ -189,11 +205,17 @@ func normalizeManagedPaths(cfg *Config) { if usesSlashPath(cfg.RouteConfigPath) { cfg.RouteConfigPath = filepath.ToSlash(cfg.RouteConfigPath) } - if usesSlashPath(cfg.SupportDir) { - cfg.SupportDir = filepath.ToSlash(cfg.SupportDir) + if usesSlashPath(cfg.CertDir) { + cfg.CertDir = filepath.ToSlash(cfg.CertDir) } - if usesSlashPath(cfg.OpenrestySupportDir) { - cfg.OpenrestySupportDir = filepath.ToSlash(cfg.OpenrestySupportDir) + if usesSlashPath(cfg.OpenrestyCertDir) { + cfg.OpenrestyCertDir = filepath.ToSlash(cfg.OpenrestyCertDir) + } + if usesSlashPath(cfg.LuaDir) { + cfg.LuaDir = filepath.ToSlash(cfg.LuaDir) + } + if usesSlashPath(cfg.OpenrestyLuaDir) { + cfg.OpenrestyLuaDir = filepath.ToSlash(cfg.OpenrestyLuaDir) } if usesSlashPath(cfg.StatePath) { cfg.StatePath = filepath.ToSlash(cfg.StatePath) diff --git a/atsf_agent/internal/config/config_test.go b/atsf_agent/internal/config/config_test.go index e5da0747..0cfce1cc 100644 --- a/atsf_agent/internal/config/config_test.go +++ b/atsf_agent/internal/config/config_test.go @@ -39,8 +39,11 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) { if cfg.RouteConfigPath != filepath.Join(dir, "data", defaultDockerRouteConfigRelativePath) { t.Fatalf("unexpected route config path: %s", cfg.RouteConfigPath) } - if cfg.SupportDir != filepath.Join(dir, "data", defaultSupportDirRelativePath) { - t.Fatalf("unexpected support dir: %s", cfg.SupportDir) + if cfg.CertDir != filepath.Join(dir, "data", defaultCertDirRelativePath) { + t.Fatalf("unexpected cert dir: %s", cfg.CertDir) + } + if cfg.LuaDir != filepath.Join(dir, "data", defaultLuaDirRelativePath) { + t.Fatalf("unexpected lua dir: %s", cfg.LuaDir) } if cfg.OpenrestyContainerName != "atsflare-openresty" { t.Fatalf("unexpected openresty container name: %s", cfg.OpenrestyContainerName) @@ -48,8 +51,11 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) { if cfg.OpenrestyDockerImage != "openresty/openresty:alpine" { t.Fatalf("unexpected openresty image: %s", cfg.OpenrestyDockerImage) } - if cfg.OpenrestySupportDir != defaultDockerOpenRestySupportDir { - t.Fatalf("unexpected openresty support dir: %s", cfg.OpenrestySupportDir) + if cfg.OpenrestyCertDir != defaultDockerOpenRestyCertDir { + t.Fatalf("unexpected openresty cert dir: %s", cfg.OpenrestyCertDir) + } + if cfg.OpenrestyLuaDir != defaultDockerOpenRestyLuaDir { + t.Fatalf("unexpected openresty lua dir: %s", cfg.OpenrestyLuaDir) } if cfg.StatePath != filepath.Join(dir, "data", defaultDockerStateRelativePath) { t.Fatalf("unexpected state path: %s", cfg.StatePath) @@ -103,8 +109,11 @@ func TestLoadPathModeKeepsExplicitPaths(t *testing.T) { if cfg.ObservabilityBufferPath != filepath.Join(dir, "data", defaultObservabilityBufferRelativePath) { t.Fatalf("unexpected observability buffer path: %s", cfg.ObservabilityBufferPath) } - if cfg.OpenrestySupportDir != cfg.SupportDir { - t.Fatalf("expected path mode openresty support dir to equal support dir, got %s / %s", cfg.OpenrestySupportDir, cfg.SupportDir) + if cfg.OpenrestyCertDir != cfg.CertDir { + t.Fatalf("expected path mode openresty cert dir to equal cert dir, got %s / %s", cfg.OpenrestyCertDir, cfg.CertDir) + } + if cfg.OpenrestyLuaDir != cfg.LuaDir { + t.Fatalf("expected path mode openresty lua dir to equal lua dir, got %s / %s", cfg.OpenrestyLuaDir, cfg.LuaDir) } if cfg.OpenrestyObservabilityPort != defaultOpenRestyObservabilityPort { t.Fatalf("unexpected path mode openresty observability port: %d", cfg.OpenrestyObservabilityPort) @@ -146,8 +155,11 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { if cfg.ObservabilityBufferPath != "/srv/atsflare/"+defaultObservabilityBufferRelativePath { t.Fatalf("unexpected observability buffer path: %s", cfg.ObservabilityBufferPath) } - if cfg.SupportDir != "/srv/atsflare/"+defaultSupportDirRelativePath { - t.Fatalf("unexpected support dir: %s", cfg.SupportDir) + if cfg.CertDir != "/srv/atsflare/"+defaultCertDirRelativePath { + t.Fatalf("unexpected cert dir: %s", cfg.CertDir) + } + if cfg.LuaDir != "/srv/atsflare/"+defaultLuaDirRelativePath { + t.Fatalf("unexpected lua dir: %s", cfg.LuaDir) } } diff --git a/atsf_agent/internal/nginx/manager.go b/atsf_agent/internal/nginx/manager.go index f7bebe43..c0c5eab2 100644 --- a/atsf_agent/internal/nginx/manager.go +++ b/atsf_agent/internal/nginx/manager.go @@ -18,7 +18,7 @@ import ( "atsflare-agent/internal/protocol" ) -const SupportDirPlaceholder = "__ATSF_SUPPORT_DIR__" +const CertDirPlaceholder = "__ATSF_CERT_DIR__" const RouteConfigPlaceholder = "__ATSF_ROUTE_CONFIG__" const AccessLogPlaceholder = "__ATSF_ACCESS_LOG__" const LuaDirPlaceholder = "__ATSF_LUA_DIR__" @@ -106,8 +106,10 @@ type DockerExecutor struct { Image string MainConfigPath string RouteConfigDir string - SupportDir string - NginxSupportDir string + CertDir string + NginxCertDir string + LuaDir string + NginxLuaDir string OpenrestyObservabilityPort int Runner CommandRunner } @@ -188,7 +190,8 @@ func (e *DockerExecutor) runContainer(ctx context.Context) error { "-p", fmt.Sprintf("127.0.0.1:%d:%d", e.OpenrestyObservabilityPort, e.OpenrestyObservabilityPort), "-v", fmt.Sprintf("%s:%s", e.MainConfigPath, DockerMainConfigPath), "-v", fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir), - "-v", fmt.Sprintf("%s:%s", e.SupportDir, e.NginxSupportDir), + "-v", fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir), + "-v", fmt.Sprintf("%s:%s", e.LuaDir, e.NginxLuaDir), e.Image, } runOutput, runErr := e.Runner.Run(ctx, e.DockerBinary, runArgs...) @@ -203,21 +206,28 @@ type Manager struct { MainConfigPath string RouteConfigPath string RuntimeRouteConfigPath string - SupportDir string - NginxSupportDir string + CertDir string + NginxCertDir string + LuaDir string + NginxLuaDir string OpenrestyObservabilityListen string OpenrestyObservabilityPort int Executor Executor } func (m *Manager) Apply(ctx context.Context, mainConfig string, routeConfig string, supportFiles []protocol.SupportFile) error { - slog.Info("openresty apply started", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath, "support_files", len(supportFiles)) + slog.Info("openresty apply started", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath, "cert_files", len(supportFiles)) backup, err := m.backup() if err != nil { return err } - if err = m.writeSupportFiles(supportFiles); err != nil { - slog.Error("writing support files failed, restoring backup", "error", err) + if err = m.EnsureLuaAssets(); err != nil { + slog.Error("writing lua assets failed, restoring backup", "error", err) + _ = m.restore(backup) + return err + } + if err = m.writeCertFiles(supportFiles); err != nil { + slog.Error("writing cert files failed, restoring backup", "error", err) _ = m.restore(backup) return err } @@ -247,6 +257,31 @@ func (m *Manager) Apply(ctx context.Context, mainConfig string, routeConfig stri return nil } +func (m *Manager) EnsureLuaAssets() error { + if strings.TrimSpace(m.LuaDir) == "" { + return nil + } + if err := os.RemoveAll(m.LuaDir); err != nil && !os.IsNotExist(err) { + return err + } + if err := os.MkdirAll(m.LuaDir, 0o755); err != nil { + return err + } + for _, file := range ManagedObservabilityLuaFiles() { + targetPath, err := luaFileTargetPath(m.LuaDir, file.Path) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { + return err + } + if err := os.WriteFile(targetPath, []byte(file.Content), 0o644); err != nil { + return err + } + } + return nil +} + func (m *Manager) EnsureRuntime(ctx context.Context, recreate bool) error { if m.Executor == nil { return errors.New("executor 未配置") @@ -308,15 +343,15 @@ func (m *Manager) CurrentChecksum() (string, error) { normalizedMain = strings.ReplaceAll(normalizedMain, fmt.Sprintf("%d", m.OpenrestyObservabilityPort), ObservabilityPortPlaceholder) } normalizedRoute := string(data) - if m.NginxSupportDir != "" { - normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxSupportDir, SupportDirPlaceholder) + if m.NginxCertDir != "" { + normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxCertDir, CertDirPlaceholder) } - files, err := m.readSupportFiles() + files, err := m.readCertFiles() if err != nil { return "", err } result := bundleChecksum(normalizedMain, normalizedRoute, files) - slog.Debug("openresty current checksum calculated", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath, "checksum", result, "support_files", len(files)) + slog.Debug("openresty current checksum calculated", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath, "checksum", result, "cert_files", len(files)) return result, nil } @@ -327,8 +362,10 @@ type ExecutorOptions struct { Image string MainConfigPath string RouteConfigPath string - SupportDir string - NginxSupportDir string + CertDir string + NginxCertDir string + LuaDir string + NginxLuaDir string OpenrestyObservabilityPort int } @@ -341,16 +378,28 @@ func NewExecutor(options ExecutorOptions) Executor { } } mainConfigPath := options.MainConfigPath - if absPath, err := filepath.Abs(mainConfigPath); err == nil { - mainConfigPath = absPath + if mainConfigPath != "" { + if absPath, err := filepath.Abs(mainConfigPath); err == nil { + mainConfigPath = absPath + } } routeConfigDir := filepath.Dir(options.RouteConfigPath) - if absDir, err := filepath.Abs(routeConfigDir); err == nil { - routeConfigDir = absDir + if options.RouteConfigPath != "" { + if absDir, err := filepath.Abs(routeConfigDir); err == nil { + routeConfigDir = absDir + } } - supportDir := options.SupportDir - if absDir, err := filepath.Abs(supportDir); err == nil { - supportDir = absDir + certDir := options.CertDir + if certDir != "" { + if absDir, err := filepath.Abs(certDir); err == nil { + certDir = absDir + } + } + luaDir := options.LuaDir + if luaDir != "" { + if absDir, err := filepath.Abs(luaDir); err == nil { + luaDir = absDir + } } return &DockerExecutor{ DockerBinary: options.DockerBinary, @@ -358,8 +407,10 @@ func NewExecutor(options ExecutorOptions) Executor { Image: options.Image, MainConfigPath: mainConfigPath, RouteConfigDir: routeConfigDir, - SupportDir: supportDir, - NginxSupportDir: options.NginxSupportDir, + CertDir: certDir, + NginxCertDir: options.NginxCertDir, + LuaDir: luaDir, + NginxLuaDir: options.NginxLuaDir, OpenrestyObservabilityPort: options.OpenrestyObservabilityPort, Runner: runner, } @@ -432,7 +483,9 @@ func (e *DockerExecutor) runEphemeralRuntimeCommandWithBinary(ctx context.Contex "-v", fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir), "-v", - fmt.Sprintf("%s:%s", e.SupportDir, e.NginxSupportDir), + fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir), + "-v", + fmt.Sprintf("%s:%s", e.LuaDir, e.NginxLuaDir), e.Image, runtimeBinary, } @@ -465,8 +518,8 @@ func (m *Manager) backup() (*backupState, error) { if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil { return nil, err } - if m.SupportDir != "" { - if err := os.MkdirAll(m.SupportDir, 0o755); err != nil { + if m.CertDir != "" { + if err := os.MkdirAll(m.CertDir, 0o755); err != nil { return nil, err } } @@ -485,12 +538,12 @@ func (m *Manager) backup() (*backupState, error) { } else if !os.IsNotExist(err) { return nil, err } - files, err := m.readSupportFiles() + files, err := m.readCertFiles() if err != nil { return nil, err } state.Files = files - slog.Debug("backup captured", "main_exists", state.MainExisted, "route_exists", state.RouteExisted, "support_files", len(state.Files)) + slog.Debug("backup captured", "main_exists", state.MainExisted, "route_exists", state.RouteExisted, "cert_files", len(state.Files)) return state, nil } @@ -498,7 +551,7 @@ func (m *Manager) restore(state *backupState) error { if state == nil { return nil } - slog.Warn("restoring nginx backup", "main_existed", state.MainExisted, "route_existed", state.RouteExisted, "support_files", len(state.Files)) + slog.Warn("restoring nginx backup", "main_existed", state.MainExisted, "route_existed", state.RouteExisted, "cert_files", len(state.Files)) if state.MainExisted { if err := os.WriteFile(m.MainConfigPath, state.MainData, 0o644); err != nil { return err @@ -513,67 +566,67 @@ func (m *Manager) restore(state *backupState) error { } else if err := os.Remove(m.RouteConfigPath); err != nil && !os.IsNotExist(err) { return err } - if m.SupportDir == "" { + if m.CertDir == "" { return nil } - if err := os.RemoveAll(m.SupportDir); err != nil && !os.IsNotExist(err) { + if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) { return err } - if err := os.MkdirAll(m.SupportDir, 0o755); err != nil { + if err := os.MkdirAll(m.CertDir, 0o755); err != nil { return err } for _, file := range state.Files { - targetPath, err := m.supportFileTargetPath(file.Path) + targetPath, err := m.certFileTargetPath(file.Path) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { return err } - if err := os.WriteFile(targetPath, []byte(file.Content), supportFileMode(file.Path)); err != nil { + if err := os.WriteFile(targetPath, []byte(file.Content), certFileMode(file.Path)); err != nil { return err } } return nil } -func (m *Manager) writeSupportFiles(supportFiles []protocol.SupportFile) error { - if m.SupportDir == "" { +func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error { + if m.CertDir == "" { return nil } - if err := os.RemoveAll(m.SupportDir); err != nil && !os.IsNotExist(err) { + if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) { return err } - if err := os.MkdirAll(m.SupportDir, 0o755); err != nil { + if err := os.MkdirAll(m.CertDir, 0o755); err != nil { return err } - for _, file := range supportFiles { - targetPath, err := m.supportFileTargetPath(file.Path) + for _, file := range certFiles { + targetPath, err := m.certFileTargetPath(file.Path) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { return err } - if err := os.WriteFile(targetPath, []byte(file.Content), supportFileMode(file.Path)); err != nil { + if err := os.WriteFile(targetPath, []byte(file.Content), certFileMode(file.Path)); err != nil { return err } } return nil } -func (m *Manager) readSupportFiles() ([]protocol.SupportFile, error) { - if m.SupportDir == "" { +func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) { + if m.CertDir == "" { return nil, nil } - if _, err := os.Stat(m.SupportDir); err != nil { + if _, err := os.Stat(m.CertDir); err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } files := make([]protocol.SupportFile, 0) - err := filepath.Walk(m.SupportDir, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(m.CertDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -584,7 +637,7 @@ func (m *Manager) readSupportFiles() ([]protocol.SupportFile, error) { if err != nil { return err } - relativePath, err := filepath.Rel(m.SupportDir, path) + relativePath, err := filepath.Rel(m.CertDir, path) if err != nil { return err } @@ -603,9 +656,9 @@ func (m *Manager) readSupportFiles() ([]protocol.SupportFile, error) { return files, nil } -func (m *Manager) supportFileTargetPath(relativePath string) (string, error) { - if strings.TrimSpace(m.SupportDir) == "" { - return "", errors.New("support dir 不能为空") +func (m *Manager) certFileTargetPath(relativePath string) (string, error) { + if strings.TrimSpace(m.CertDir) == "" { + return "", errors.New("cert dir 不能为空") } candidate := strings.TrimSpace(relativePath) if strings.Contains(candidate, `\`) { @@ -613,25 +666,25 @@ func (m *Manager) supportFileTargetPath(relativePath string) (string, error) { } normalizedPath := filepath.Clean(filepath.FromSlash(candidate)) if normalizedPath == "." || normalizedPath == "" { - return "", errors.New("support file path 不能为空") + return "", errors.New("cert file path 不能为空") } if filepath.IsAbs(normalizedPath) || filepath.VolumeName(normalizedPath) != "" { - return "", fmt.Errorf("support file path %q must be relative", relativePath) + return "", fmt.Errorf("cert file path %q must be relative", relativePath) } - targetPath := filepath.Join(m.SupportDir, normalizedPath) - relativeToBase, err := filepath.Rel(m.SupportDir, targetPath) + targetPath := filepath.Join(m.CertDir, normalizedPath) + relativeToBase, err := filepath.Rel(m.CertDir, targetPath) if err != nil { return "", err } if relativeToBase == ".." || strings.HasPrefix(relativeToBase, ".."+string(os.PathSeparator)) { - return "", fmt.Errorf("support file path %q escapes support dir", relativePath) + return "", fmt.Errorf("cert file path %q escapes cert dir", relativePath) } return targetPath, nil } -func supportFileMode(relativePath string) fs.FileMode { +func certFileMode(relativePath string) fs.FileMode { switch strings.ToLower(filepath.Ext(strings.TrimSpace(relativePath))) { - case ".lua", ".crt", ".pem": + case ".crt", ".pem": return 0o644 case ".key": return 0o600 @@ -640,11 +693,37 @@ func supportFileMode(relativePath string) fs.FileMode { } } +func luaFileTargetPath(baseDir string, relativePath string) (string, error) { + if strings.TrimSpace(baseDir) == "" { + return "", errors.New("lua dir 不能为空") + } + candidate := strings.TrimSpace(relativePath) + if strings.Contains(candidate, `\`) { + candidate = strings.ReplaceAll(candidate, `\`, "/") + } + normalizedPath := filepath.Clean(filepath.FromSlash(candidate)) + if normalizedPath == "." || normalizedPath == "" { + return "", errors.New("lua file path 不能为空") + } + if filepath.IsAbs(normalizedPath) || filepath.VolumeName(normalizedPath) != "" { + return "", fmt.Errorf("lua file path %q must be relative", relativePath) + } + targetPath := filepath.Join(baseDir, normalizedPath) + relativeToBase, err := filepath.Rel(baseDir, targetPath) + if err != nil { + return "", err + } + if relativeToBase == ".." || strings.HasPrefix(relativeToBase, ".."+string(os.PathSeparator)) { + return "", fmt.Errorf("lua file path %q escapes lua dir", relativePath) + } + return targetPath, nil +} + func (m *Manager) renderRouteConfig(content string) string { - if m.NginxSupportDir == "" { + if m.NginxCertDir == "" { return content } - return strings.ReplaceAll(content, SupportDirPlaceholder, m.NginxSupportDir) + return strings.ReplaceAll(content, CertDirPlaceholder, m.NginxCertDir) } func (m *Manager) renderMainConfig(content string) string { @@ -693,10 +772,10 @@ func (m *Manager) accessLogRuntimePath() string { } func (m *Manager) luaRuntimePath() string { - if strings.TrimSpace(m.NginxSupportDir) == "" { + if strings.TrimSpace(m.NginxLuaDir) == "" { return "" } - return filepath.ToSlash(m.NginxSupportDir) + return filepath.ToSlash(m.NginxLuaDir) } func checksum(content string) string { diff --git a/atsf_agent/internal/nginx/manager_test.go b/atsf_agent/internal/nginx/manager_test.go index b7b21e23..de4d35a2 100644 --- a/atsf_agent/internal/nginx/manager_test.go +++ b/atsf_agent/internal/nginx/manager_test.go @@ -117,14 +117,16 @@ func TestDockerExecutorCheckHealthFailsWhenContainerStopped(t *testing.T) { }, } executor := &DockerExecutor{ - DockerBinary: "docker", - ContainerName: "atsflare-openresty", - Image: "openresty/openresty:alpine", - MainConfigPath: filepath.Clean("/tmp/nginx.conf"), - RouteConfigDir: filepath.Clean("/tmp/routes"), - SupportDir: filepath.Clean("/tmp/support"), - NginxSupportDir: "/etc/nginx/atsflare-support", - Runner: runner, + DockerBinary: "docker", + ContainerName: "atsflare-openresty", + Image: "openresty/openresty:alpine", + MainConfigPath: filepath.Clean("/tmp/nginx.conf"), + RouteConfigDir: filepath.Clean("/tmp/routes"), + CertDir: filepath.Clean("/tmp/certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Clean("/tmp/lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", + Runner: runner, } if err := executor.CheckHealth(context.Background()); err == nil { t.Fatal("expected CheckHealth to fail when container is not running") @@ -141,14 +143,16 @@ func TestDockerExecutorStartsContainerWhenMissing(t *testing.T) { }, } executor := &DockerExecutor{ - DockerBinary: "docker", - ContainerName: "atsflare-openresty", - Image: "openresty/openresty:alpine", - MainConfigPath: filepath.Clean("/tmp/nginx.conf"), - RouteConfigDir: filepath.Clean("/tmp/routes"), - SupportDir: filepath.Clean("/tmp/support"), - NginxSupportDir: "/etc/nginx/atsflare-support", - Runner: runner, + DockerBinary: "docker", + ContainerName: "atsflare-openresty", + Image: "openresty/openresty:alpine", + MainConfigPath: filepath.Clean("/tmp/nginx.conf"), + RouteConfigDir: filepath.Clean("/tmp/routes"), + CertDir: filepath.Clean("/tmp/certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Clean("/tmp/lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", + Runner: runner, } if err := executor.Test(context.Background()); err != nil { @@ -176,14 +180,16 @@ func TestDockerExecutorStartsStoppedContainer(t *testing.T) { }, } executor := &DockerExecutor{ - DockerBinary: "docker", - ContainerName: "atsflare-openresty", - Image: "openresty/openresty:alpine", - MainConfigPath: filepath.Clean("/tmp/nginx.conf"), - RouteConfigDir: filepath.Clean("/tmp/routes"), - SupportDir: filepath.Clean("/tmp/support"), - NginxSupportDir: "/etc/nginx/atsflare-support", - Runner: runner, + DockerBinary: "docker", + ContainerName: "atsflare-openresty", + Image: "openresty/openresty:alpine", + MainConfigPath: filepath.Clean("/tmp/nginx.conf"), + RouteConfigDir: filepath.Clean("/tmp/routes"), + CertDir: filepath.Clean("/tmp/certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Clean("/tmp/lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", + Runner: runner, } if err := executor.Reload(context.Background()); err != nil { @@ -207,7 +213,8 @@ func TestDockerExecutorStartsStoppedContainer(t *testing.T) { func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) { mainConfigPath := filepath.Clean("/tmp/managed/nginx.conf") routeConfigDir := filepath.Clean("/tmp/managed/conf.d") - supportDir := filepath.Clean("/tmp/managed/support") + certDir := filepath.Clean("/tmp/managed/certs") + luaDir := filepath.Clean("/tmp/managed/lua") runner := &fakeRunner{} executor := &DockerExecutor{ DockerBinary: "docker", @@ -215,8 +222,10 @@ func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) { Image: "openresty/openresty:alpine", MainConfigPath: mainConfigPath, RouteConfigDir: routeConfigDir, - SupportDir: supportDir, - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: certDir, + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: luaDir, + NginxLuaDir: "/etc/nginx/atsflare-lua", OpenrestyObservabilityPort: 18081, Runner: runner, } @@ -237,7 +246,8 @@ func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) { "-p", "127.0.0.1:18081:18081", "-v", mainConfigPath + ":" + DockerMainConfigPath, "-v", routeConfigDir + ":/etc/nginx/conf.d", - "-v", supportDir + ":/etc/nginx/atsflare-support", + "-v", certDir + ":/etc/nginx/atsflare-certs", + "-v", luaDir + ":/etc/nginx/atsflare-lua", "openresty/openresty:alpine", } if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) { @@ -260,8 +270,10 @@ func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) { Image: "openresty/openresty:alpine", MainConfigPath: filepath.Clean("/tmp/nginx.conf"), RouteConfigDir: filepath.Clean("/tmp/routes"), - SupportDir: filepath.Clean("/tmp/support"), - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: filepath.Clean("/tmp/certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Clean("/tmp/lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", OpenrestyObservabilityPort: 18081, Runner: runner, } @@ -287,8 +299,10 @@ func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) { Image: "openresty/openresty:alpine", MainConfigPath: "./data/etc/nginx/nginx.conf", RouteConfigPath: "./data/etc/nginx/conf.d/atsflare_routes.conf", - SupportDir: "./data/etc/nginx/support", - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: "./data/etc/nginx/certs", + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: "./data/etc/nginx/lua", + NginxLuaDir: "/etc/nginx/atsflare-lua", OpenrestyObservabilityPort: 18081, }) @@ -330,19 +344,21 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { tempDir := t.TempDir() mainPath := filepath.Join(tempDir, "nginx.conf") routePath := filepath.Join(tempDir, "conf.d", "atsflare_routes.conf") - supportDir := filepath.Join(tempDir, "support") + certDir := filepath.Join(tempDir, "certs") manager := &Manager{ MainConfigPath: mainPath, RouteConfigPath: routePath, - SupportDir: supportDir, - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: certDir, + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Join(tempDir, "lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", Executor: &fakeExecutor{}, } err := manager.Apply( context.Background(), "include __ATSF_ROUTE_CONFIG__;\naccess_log __ATSF_ACCESS_LOG__ atsflare_json;\n", - "ssl_certificate __ATSF_SUPPORT_DIR__/1.crt;\n", + "ssl_certificate __ATSF_CERT_DIR__/1.crt;\n", []protocol.SupportFile{{Path: "1.crt", Content: "cert"}}, ) if err != nil { @@ -362,7 +378,7 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { if err != nil { t.Fatalf("failed to read route config: %v", err) } - if string(routeData) != "ssl_certificate /etc/nginx/atsflare-support/1.crt;\n" { + if string(routeData) != "ssl_certificate /etc/nginx/atsflare-certs/1.crt;\n" { t.Fatalf("unexpected route config: %s", string(routeData)) } @@ -372,7 +388,7 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { } expected := bundleChecksum( "include __ATSF_ROUTE_CONFIG__;\naccess_log __ATSF_ACCESS_LOG__ atsflare_json;\n", - "ssl_certificate __ATSF_SUPPORT_DIR__/1.crt;\n", + "ssl_certificate __ATSF_CERT_DIR__/1.crt;\n", []protocol.SupportFile{{Path: "1.crt", Content: "cert"}}, ) if value != expected { @@ -388,8 +404,10 @@ func TestManagerApplyUsesRuntimeRouteConfigPath(t *testing.T) { MainConfigPath: mainPath, RouteConfigPath: routePath, RuntimeRouteConfigPath: DockerRouteConfigPath, - SupportDir: filepath.Join(tempDir, "support"), - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: filepath.Join(tempDir, "certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Join(tempDir, "lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", Executor: &fakeExecutor{}, } @@ -462,13 +480,15 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { manager := &Manager{ MainConfigPath: filepath.Join(tempDir, "nginx.conf"), RouteConfigPath: filepath.Join(tempDir, "routes.conf"), - SupportDir: filepath.Join(tempDir, "support"), - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: filepath.Join(tempDir, "certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Join(tempDir, "lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", OpenrestyObservabilityListen: "18081", Executor: &fakeExecutor{}, } - err := manager.Apply(context.Background(), "include __ATSF_ROUTE_CONFIG__;\nserver { listen __ATSF_OBSERVABILITY_LISTEN__; }", "ssl_certificate __ATSF_SUPPORT_DIR__/1.crt;", []protocol.SupportFile{ + err := manager.Apply(context.Background(), "include __ATSF_ROUTE_CONFIG__;\nserver { listen __ATSF_OBSERVABILITY_LISTEN__; }", "ssl_certificate __ATSF_CERT_DIR__/1.crt;", []protocol.SupportFile{ {Path: "1.crt", Content: "cert-data"}, {Path: "1.key", Content: "key-data"}, }) @@ -480,7 +500,7 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { if err != nil { t.Fatalf("failed to read route config: %v", err) } - if !strings.Contains(string(routeData), "/etc/nginx/atsflare-support/1.crt") { + if !strings.Contains(string(routeData), "/etc/nginx/atsflare-certs/1.crt") { t.Fatalf("expected placeholder replacement in route config, got %s", string(routeData)) } mainData, err := os.ReadFile(manager.MainConfigPath) @@ -490,25 +510,27 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { if !strings.Contains(string(mainData), "listen 18081;") { t.Fatalf("expected observability listen placeholder replacement in main config, got %s", string(mainData)) } - certData, err := os.ReadFile(filepath.Join(manager.SupportDir, "1.crt")) + certData, err := os.ReadFile(filepath.Join(manager.CertDir, "1.crt")) if err != nil { t.Fatalf("failed to read cert file: %v", err) } if string(certData) != "cert-data" { t.Fatalf("unexpected cert file content: %s", string(certData)) } - luaInfo, err := os.Stat(filepath.Join(manager.SupportDir, "observability", "log.lua")) - if err == nil { - t.Fatalf("expected no lua file in this test, got %v", luaInfo) + luaInfo, err := os.Stat(filepath.Join(manager.LuaDir, "log.lua")) + if err != nil { + t.Fatalf("expected managed lua file to exist, stat err = %v", err) + } + if luaInfo.Mode().Perm() != 0o644 { + t.Fatalf("unexpected lua mode: %o", luaInfo.Mode().Perm()) } } -func TestSupportFileMode(t *testing.T) { +func TestCertFileMode(t *testing.T) { testCases := []struct { path string want os.FileMode }{ - {path: "observability/log.lua", want: 0o644}, {path: "1.crt", want: 0o644}, {path: "1.pem", want: 0o644}, {path: "1.key", want: 0o600}, @@ -516,53 +538,39 @@ func TestSupportFileMode(t *testing.T) { } for _, testCase := range testCases { - if got := supportFileMode(testCase.path); got != testCase.want { + if got := certFileMode(testCase.path); got != testCase.want { t.Fatalf("unexpected mode for %s: got %o want %o", testCase.path, got, testCase.want) } } } -func TestManagerApplyWritesLuaSupportFilesReadable(t *testing.T) { +func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) { tempDir := t.TempDir() manager := &Manager{ - MainConfigPath: filepath.Join(tempDir, "nginx.conf"), - RouteConfigPath: filepath.Join(tempDir, "routes.conf"), - SupportDir: filepath.Join(tempDir, "support"), - NginxSupportDir: "/etc/nginx/atsflare-support", - Executor: &fakeExecutor{}, + LuaDir: filepath.Join(tempDir, "lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", } - err := manager.Apply(context.Background(), "main", "route", []protocol.SupportFile{ - {Path: "observability/log.lua", Content: "return"}, - {Path: "1.key", Content: "secret"}, - }) + err := manager.EnsureLuaAssets() if err != nil { - t.Fatalf("Apply failed: %v", err) + t.Fatalf("EnsureLuaAssets failed: %v", err) } - luaInfo, err := os.Stat(filepath.Join(manager.SupportDir, "observability", "log.lua")) + luaInfo, err := os.Stat(filepath.Join(manager.LuaDir, "log.lua")) if err != nil { t.Fatalf("failed to stat lua file: %v", err) } if luaInfo.Mode().Perm() != 0o644 { t.Fatalf("unexpected lua mode: %o", luaInfo.Mode().Perm()) } - - keyInfo, err := os.Stat(filepath.Join(manager.SupportDir, "1.key")) - if err != nil { - t.Fatalf("failed to stat key file: %v", err) - } - if keyInfo.Mode().Perm() != 0o600 { - t.Fatalf("unexpected key mode: %o", keyInfo.Mode().Perm()) - } } -func TestManagerRollbackRestoresSupportFiles(t *testing.T) { +func TestManagerRollbackRestoresCertFiles(t *testing.T) { tempDir := t.TempDir() routePath := filepath.Join(tempDir, "routes.conf") mainPath := filepath.Join(tempDir, "nginx.conf") - supportDir := filepath.Join(tempDir, "support") - if err := os.MkdirAll(supportDir, 0o755); err != nil { + certDir := filepath.Join(tempDir, "certs") + if err := os.MkdirAll(certDir, 0o755); err != nil { t.Fatalf("MkdirAll failed: %v", err) } if err := os.WriteFile(mainPath, []byte("old-main"), 0o644); err != nil { @@ -571,14 +579,16 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) { if err := os.WriteFile(routePath, []byte("old-route"), 0o644); err != nil { t.Fatalf("WriteFile failed: %v", err) } - if err := os.WriteFile(filepath.Join(supportDir, "1.crt"), []byte("old-cert"), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(certDir, "1.crt"), []byte("old-cert"), 0o600); err != nil { t.Fatalf("WriteFile failed: %v", err) } manager := &Manager{ MainConfigPath: mainPath, RouteConfigPath: routePath, - SupportDir: supportDir, - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: certDir, + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Join(tempDir, "lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", Executor: &fakeExecutor{ testErr: errors.New("openresty test failed"), }, @@ -605,7 +615,7 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) { if string(routeData) != "old-route" { t.Fatalf("expected route rollback, got %s", string(routeData)) } - certData, err := os.ReadFile(filepath.Join(supportDir, "1.crt")) + certData, err := os.ReadFile(filepath.Join(certDir, "1.crt")) if err != nil { t.Fatalf("failed to read cert file: %v", err) } @@ -614,9 +624,9 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) { } } -func TestManagerSupportFileTargetPathRejectsEscapes(t *testing.T) { - manager := &Manager{SupportDir: filepath.Join(t.TempDir(), "support")} - if err := os.MkdirAll(manager.SupportDir, 0o755); err != nil { +func TestManagerCertFileTargetPathRejectsEscapes(t *testing.T) { + manager := &Manager{CertDir: filepath.Join(t.TempDir(), "certs")} + if err := os.MkdirAll(manager.CertDir, 0o755); err != nil { t.Fatalf("MkdirAll failed: %v", err) } @@ -637,7 +647,7 @@ func TestManagerSupportFileTargetPathRejectsEscapes(t *testing.T) { } for _, testCase := range testCases { - targetPath, err := manager.supportFileTargetPath(testCase.path) + targetPath, err := manager.certFileTargetPath(testCase.path) if testCase.shouldErr { if err == nil { t.Fatalf("expected path %q to be rejected, got target %q", testCase.path, targetPath) @@ -647,19 +657,21 @@ func TestManagerSupportFileTargetPathRejectsEscapes(t *testing.T) { if err != nil { t.Fatalf("expected path %q to be accepted: %v", testCase.path, err) } - if !strings.HasPrefix(targetPath, manager.SupportDir) { - t.Fatalf("expected target path %q to stay under %q", targetPath, manager.SupportDir) + if !strings.HasPrefix(targetPath, manager.CertDir) { + t.Fatalf("expected target path %q to stay under %q", targetPath, manager.CertDir) } } } -func TestManagerApplyRejectsSupportFilePathTraversal(t *testing.T) { +func TestManagerApplyRejectsCertFilePathTraversal(t *testing.T) { tempDir := t.TempDir() manager := &Manager{ MainConfigPath: filepath.Join(tempDir, "nginx.conf"), RouteConfigPath: filepath.Join(tempDir, "routes.conf"), - SupportDir: filepath.Join(tempDir, "support"), - NginxSupportDir: "/etc/nginx/atsflare-support", + CertDir: filepath.Join(tempDir, "certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + LuaDir: filepath.Join(tempDir, "lua"), + NginxLuaDir: "/etc/nginx/atsflare-lua", Executor: &fakeExecutor{}, } diff --git a/atsf_agent/internal/nginx/observability_assets.go b/atsf_agent/internal/nginx/observability_assets.go new file mode 100644 index 00000000..fcddfab5 --- /dev/null +++ b/atsf_agent/internal/nginx/observability_assets.go @@ -0,0 +1,158 @@ +package nginx + +import "atsflare-agent/internal/protocol" + +const ( + openRestyObservabilityWindowTTL = 7200 + openRestyObservabilityWindowSize = 60 +) + +const openRestyObservabilityInitLua = `local dict = ngx.shared.atsflare_observability +if not dict then + return +end + +return +` + +const openRestyObservabilityLogLua = `local dict = ngx.shared.atsflare_observability +if not dict then + return +end + +local request_uri = tostring(ngx.var.uri or "") +if request_uri == "/atsflare/observability" or request_uri == "/atsflare/stub_status" then + return +end + +local ttl = ` + "7200" + ` +local now = ngx.time() +local window_size = ` + "60" + ` +local window_start = now - (now % window_size) + +local function ensure_counter(key) + dict:add(key, 0, ttl) +end + +local function incr(key, delta) + ensure_counter(key) + local value, err = dict:incr(key, delta) + if not value and err == "not found" then + dict:set(key, delta, ttl) + end +end + +local function remember_value(list_key, marker_key, value) + if value == "" then + return + end + if not dict:add(marker_key, 1, ttl) then + return + end + local existing = dict:get(list_key) + if not existing or existing == "" then + dict:set(list_key, value, ttl) + return + end + dict:set(list_key, existing .. "\n" .. value, ttl) +end + +local window_prefix = tostring(window_start) +incr("request_count:" .. window_prefix, 1) + +local status = tostring(ngx.status or 0) +if status ~= "0" then + incr("status:" .. window_prefix .. ":" .. status, 1) + remember_value( + "status_keys:" .. window_prefix, + "status_marker:" .. window_prefix .. ":" .. status, + status + ) + if tonumber(status) and tonumber(status) >= 500 then + incr("error_count:" .. window_prefix, 1) + end +end + +local host = tostring(ngx.var.host or "") +if host ~= "" then + incr("domain:" .. window_prefix .. ":" .. host, 1) + remember_value( + "domain_keys:" .. window_prefix, + "domain_marker:" .. window_prefix .. ":" .. host, + host + ) +end + +local remote_addr = tostring(ngx.var.binary_remote_addr or ngx.var.remote_addr or "") +if remote_addr ~= "" and dict:add("visitor:" .. window_prefix .. ":" .. remote_addr, 1, ttl) then + incr("unique_visitor_count:" .. window_prefix, 1) +end + +local request_length = tonumber(ngx.var.request_length) or 0 +if request_length > 0 then + incr("openresty_rx_bytes:" .. window_prefix, request_length) +end + +local bytes_sent = tonumber(ngx.var.bytes_sent) or tonumber(ngx.var.body_bytes_sent) or 0 +if bytes_sent > 0 then + incr("openresty_tx_bytes:" .. window_prefix, bytes_sent) +end +` + +const openRestyObservabilityReadLua = `local cjson = require "cjson.safe" + +local dict = ngx.shared.atsflare_observability +if not dict then + ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE + ngx.say(cjson.encode({ message = "shared dict unavailable" })) + return +end + +local now = ngx.time() +local window_size = ` + "60" + ` +local window_start = now - (now % window_size) +local current_window = tostring(window_start) + +local function read_counter(key) + return tonumber(dict:get(key) or 0) or 0 +end + +local function read_map(window_id, prefix, list_key) + local result = {} + local raw = dict:get(list_key .. ":" .. window_id) + if not raw or raw == "" then + return result + end + for value in string.gmatch(raw, "[^\n]+") do + result[value] = read_counter(prefix .. ":" .. window_id .. ":" .. value) + end + return result +end + +local payload = { + window_started_at_unix = window_start, + window_ended_at_unix = now, + request_count = read_counter("request_count:" .. current_window), + error_count = read_counter("error_count:" .. current_window), + unique_visitor_count = read_counter("unique_visitor_count:" .. current_window), + status_codes = read_map(current_window, "status", "status_keys"), + top_domains = read_map(current_window, "domain", "domain_keys"), + source_countries = {}, + openresty_rx_bytes = read_counter("openresty_rx_bytes:" .. current_window), + openresty_tx_bytes = read_counter("openresty_tx_bytes:" .. current_window) +} + +ngx.header.content_type = "application/json" +ngx.say(cjson.encode(payload)) +` + +func ManagedObservabilityLuaFiles() []protocol.SupportFile { + return []protocol.SupportFile{ + {Path: "init.lua", Content: openRestyObservabilityInitLua}, + {Path: "log.lua", Content: openRestyObservabilityLogLua}, + {Path: "read.lua", Content: openRestyObservabilityReadLua}, + {Path: "observability/init.lua", Content: openRestyObservabilityInitLua}, + {Path: "observability/log.lua", Content: openRestyObservabilityLogLua}, + {Path: "observability/read.lua", Content: openRestyObservabilityReadLua}, + } +} diff --git a/atsf_server/service/agent.go b/atsf_server/service/agent.go index 627e8406..132cc7db 100644 --- a/atsf_server/service/agent.go +++ b/atsf_server/service/agent.go @@ -191,6 +191,7 @@ func GetActiveConfigForAgent() (*AgentConfigResponse, error) { return nil, err } } + supportFiles = filterCertificateSupportFiles(supportFiles) slog.Debug("agent fetched active config", "version", version.Version, "checksum", version.Checksum) return &AgentConfigResponse{ Version: version.Version, @@ -203,6 +204,21 @@ func GetActiveConfigForAgent() (*AgentConfigResponse, error) { }, nil } +func filterCertificateSupportFiles(files []SupportFile) []SupportFile { + if len(files) == 0 { + return nil + } + filtered := make([]SupportFile, 0, len(files)) + for _, file := range files { + path := strings.ToLower(strings.TrimSpace(file.Path)) + switch { + case strings.HasSuffix(path, ".crt"), strings.HasSuffix(path, ".key"), strings.HasSuffix(path, ".pem"): + filtered = append(filtered, file) + } + } + return filtered +} + func ReportApplyLog(payload ApplyLogPayload) (*model.ApplyLog, error) { now := time.Now() payload.NodeID = strings.TrimSpace(payload.NodeID) diff --git a/atsf_server/service/config_version.go b/atsf_server/service/config_version.go index 6bc1a842..6035786a 100644 --- a/atsf_server/service/config_version.go +++ b/atsf_server/service/config_version.go @@ -115,7 +115,7 @@ type configBundle struct { } const ( - nginxSupportDirPlaceholder = "__ATSF_SUPPORT_DIR__" + nginxCertDirPlaceholder = "__ATSF_CERT_DIR__" nginxRouteConfigPlaceholder = "__ATSF_ROUTE_CONFIG__" nginxAccessLogPlaceholder = "__ATSF_ACCESS_LOG__" nginxLuaDirPlaceholder = "__ATSF_LUA_DIR__" @@ -354,7 +354,6 @@ func buildCurrentConfigBundle(requireRoutes bool) (*configBundle, error) { if err != nil { return nil, err } - supportFiles = append(supportFiles, buildOpenRestyObservabilitySupportFiles()...) mainConfig := renderMainConfig(openRestyConfig) return &configBundle{ Routes: routes, @@ -752,8 +751,8 @@ func renderHTTPRedirectServer(domain string) string { } func renderHTTPSServer(domain string, originURL string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput) string { - certPath := fmt.Sprintf("%s/%s", nginxSupportDirPlaceholder, certificateCertFileName(certificateID)) - keyPath := fmt.Sprintf("%s/%s", nginxSupportDirPlaceholder, certificateKeyFileName(certificateID)) + certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) + keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID)) return fmt.Sprintf("server {\n listen 443 ssl;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(customHeaders), originURL) } diff --git a/atsf_server/service/https_phase1_test.go b/atsf_server/service/https_phase1_test.go index 93c3f042..9a9c3116 100644 --- a/atsf_server/service/https_phase1_test.go +++ b/atsf_server/service/https_phase1_test.go @@ -57,7 +57,7 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) { if !strings.Contains(result.Version.MainConfig, "access_log __ATSF_ACCESS_LOG__ atsflare_json;") { t.Fatal("expected main config to include managed access log placeholder") } - if !strings.Contains(result.Version.MainConfig, "log_by_lua_file __ATSF_LUA_DIR__/observability/log.lua;") { + if !strings.Contains(result.Version.MainConfig, "log_by_lua_file __ATSF_LUA_DIR__/log.lua;") { t.Fatal("expected main config to include managed openresty lua log hook") } if !strings.Contains(result.Version.MainConfig, "listen __ATSF_OBSERVABILITY_LISTEN__;") { @@ -72,21 +72,12 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) { if !strings.Contains(result.Version.RenderedConfig, "return 301 https://$host$request_uri;") { t.Fatal("expected rendered config to include http redirect") } - if !strings.Contains(result.Version.RenderedConfig, "__ATSF_SUPPORT_DIR__/") { - t.Fatal("expected rendered config to keep support dir placeholder for certificates") + if !strings.Contains(result.Version.RenderedConfig, "__ATSF_CERT_DIR__/") { + t.Fatal("expected rendered config to keep cert dir placeholder for certificates") } if !strings.Contains(result.Version.SupportFilesJSON, ".crt") || !strings.Contains(result.Version.SupportFilesJSON, ".key") { t.Fatal("expected support files to contain certificate and key") } - if !strings.Contains(result.Version.SupportFilesJSON, "observability/log.lua") || !strings.Contains(result.Version.SupportFilesJSON, "observability/read.lua") { - t.Fatal("expected support files to contain managed openresty observability lua scripts") - } - if !strings.Contains(result.Version.SupportFilesJSON, "/atsflare/observability") || !strings.Contains(result.Version.SupportFilesJSON, "/atsflare/stub_status") { - t.Fatal("expected observability log lua to skip self-observability requests") - } - if !strings.Contains(result.Version.SupportFilesJSON, "window_start = now - (now % window_size)") || !strings.Contains(result.Version.SupportFilesJSON, "local window_size = 60") { - t.Fatal("expected support files to use fixed 60-second observability windows") - } } func TestCreateProxyRouteRejectsHTTPSWithoutCertificate(t *testing.T) { @@ -205,7 +196,7 @@ func TestPreviewAndDiffConfigVersion(t *testing.T) { if !strings.Contains(preview.MainConfig, "include __ATSF_ROUTE_CONFIG__;") { t.Fatal("expected preview main config to include managed route config placeholder") } - if !strings.Contains(preview.MainConfig, "log_by_lua_file __ATSF_LUA_DIR__/observability/log.lua;") { + if !strings.Contains(preview.MainConfig, "log_by_lua_file __ATSF_LUA_DIR__/log.lua;") { t.Fatal("expected preview main config to include managed openresty lua log hook") } if !strings.Contains(preview.RenderedConfig, `proxy_set_header X-Release "candidate";`) { diff --git a/atsf_server/service/openresty_observability_assets.go b/atsf_server/service/openresty_observability_assets.go index e962c3ce..cdbb7e62 100644 --- a/atsf_server/service/openresty_observability_assets.go +++ b/atsf_server/service/openresty_observability_assets.go @@ -3,161 +3,11 @@ package service import "fmt" const ( - openRestyObservabilitySupportDir = "observability" - openRestyObservabilityInitLuaPath = openRestyObservabilitySupportDir + "/init.lua" - openRestyObservabilityLogLuaPath = openRestyObservabilitySupportDir + "/log.lua" - openRestyObservabilityReadLuaPath = openRestyObservabilitySupportDir + "/read.lua" - openRestyObservabilityWindowTTL = 7200 - openRestyObservabilityWindowSize = 60 + openRestyObservabilityInitLuaPath = "init.lua" + openRestyObservabilityLogLuaPath = "log.lua" + openRestyObservabilityReadLuaPath = "read.lua" ) -const openRestyObservabilityInitLua = `local dict = ngx.shared.atsflare_observability -if not dict then - return -end - -return -` - -const openRestyObservabilityLogLua = `local dict = ngx.shared.atsflare_observability -if not dict then - return -end - -local request_uri = tostring(ngx.var.uri or "") -if request_uri == "/atsflare/observability" or request_uri == "/atsflare/stub_status" then - return -end - -local ttl = ` + "7200" + ` -local now = ngx.time() -local window_size = ` + "60" + ` -local window_start = now - (now % window_size) - -local function ensure_counter(key) - dict:add(key, 0, ttl) -end - -local function incr(key, delta) - ensure_counter(key) - local value, err = dict:incr(key, delta) - if not value and err == "not found" then - dict:set(key, delta, ttl) - end -end - -local function remember_value(list_key, marker_key, value) - if value == "" then - return - end - if not dict:add(marker_key, 1, ttl) then - return - end - local existing = dict:get(list_key) - if not existing or existing == "" then - dict:set(list_key, value, ttl) - return - end - dict:set(list_key, existing .. "\n" .. value, ttl) -end - -local window_prefix = tostring(window_start) -incr("request_count:" .. window_prefix, 1) - -local status = tostring(ngx.status or 0) -if status ~= "0" then - incr("status:" .. window_prefix .. ":" .. status, 1) - remember_value( - "status_keys:" .. window_prefix, - "status_marker:" .. window_prefix .. ":" .. status, - status - ) - if tonumber(status) and tonumber(status) >= 500 then - incr("error_count:" .. window_prefix, 1) - end -end - -local host = tostring(ngx.var.host or "") -if host ~= "" then - incr("domain:" .. window_prefix .. ":" .. host, 1) - remember_value( - "domain_keys:" .. window_prefix, - "domain_marker:" .. window_prefix .. ":" .. host, - host - ) -end - -local remote_addr = tostring(ngx.var.binary_remote_addr or ngx.var.remote_addr or "") -if remote_addr ~= "" and dict:add("visitor:" .. window_prefix .. ":" .. remote_addr, 1, ttl) then - incr("unique_visitor_count:" .. window_prefix, 1) -end - -local request_length = tonumber(ngx.var.request_length) or 0 -if request_length > 0 then - incr("openresty_rx_bytes:" .. window_prefix, request_length) -end - -local bytes_sent = tonumber(ngx.var.bytes_sent) or tonumber(ngx.var.body_bytes_sent) or 0 -if bytes_sent > 0 then - incr("openresty_tx_bytes:" .. window_prefix, bytes_sent) -end -` - -const openRestyObservabilityReadLua = `local cjson = require "cjson.safe" - -local dict = ngx.shared.atsflare_observability -if not dict then - ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE - ngx.say(cjson.encode({ message = "shared dict unavailable" })) - return -end - -local now = ngx.time() -local window_size = ` + "60" + ` -local window_start = now - (now % window_size) -local current_window = tostring(window_start) - -local function read_counter(key) - return tonumber(dict:get(key) or 0) or 0 -end - -local function read_map(window_id, prefix, list_key) - local result = {} - local raw = dict:get(list_key .. ":" .. window_id) - if not raw or raw == "" then - return result - end - for value in string.gmatch(raw, "[^\n]+") do - result[value] = read_counter(prefix .. ":" .. window_id .. ":" .. value) - end - return result -end - -local payload = { - window_started_at_unix = window_start, - window_ended_at_unix = now, - request_count = read_counter("request_count:" .. current_window), - error_count = read_counter("error_count:" .. current_window), - unique_visitor_count = read_counter("unique_visitor_count:" .. current_window), - status_codes = read_map(current_window, "status", "status_keys"), - top_domains = read_map(current_window, "domain", "domain_keys"), - source_countries = {}, - openresty_rx_bytes = read_counter("openresty_rx_bytes:" .. current_window), - openresty_tx_bytes = read_counter("openresty_tx_bytes:" .. current_window) -} - -ngx.header.content_type = "application/json" -ngx.say(cjson.encode(payload)) -` - -func buildOpenRestyObservabilitySupportFiles() []SupportFile { - return []SupportFile{ - {Path: openRestyObservabilityInitLuaPath, Content: openRestyObservabilityInitLua}, - {Path: openRestyObservabilityLogLuaPath, Content: openRestyObservabilityLogLua}, - {Path: openRestyObservabilityReadLuaPath, Content: openRestyObservabilityReadLua}, - } -} - func renderOpenRestyObservabilityTemplateBlock() string { return stringsJoinLines( " lua_shared_dict atsflare_observability 10m;", diff --git a/docs/app-config.md b/docs/app-config.md index 0de64a12..78d350b3 100644 --- a/docs/app-config.md +++ b/docs/app-config.md @@ -260,8 +260,10 @@ go run ./cmd/agent -config ./agent.json "openresty_path": "/usr/local/openresty/nginx/sbin/openresty", "main_config_path": "/usr/local/openresty/nginx/conf/nginx.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf", - "support_dir": "/usr/local/openresty/nginx/conf/support", - "openresty_support_dir": "/usr/local/openresty/nginx/conf/support", + "cert_dir": "/usr/local/openresty/nginx/conf/certs", + "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs", + "lua_dir": "/usr/local/openresty/nginx/conf/lua", + "openresty_lua_dir": "/usr/local/openresty/nginx/conf/lua", "openresty_observability_port": 18081, "observability_buffer_path": "./data/observability-buffer.json", "observability_replay_minutes": 15, @@ -288,8 +290,10 @@ go run ./cmd/agent -config ./agent.json | `data_dir` | Agent 数据目录,用于存储托管配置、证书和状态文件 | 否 | 配置文件所在目录下的 `data` 子目录 | `./data` | | `main_config_path` | 第五版主配置接管时 OpenResty 主配置文件写入路径 | 第五版本机模式建议必填 | Docker 模式可使用受管默认路径;本机模式建议显式设置 | `/usr/local/openresty/nginx/conf/nginx.conf` | | `route_config_path` | 路由配置文件写入路径 | 否 | 默认为 `data_dir` 下托管路径 | `/etc/nginx/conf.d/atsflare_routes.conf` | -| `support_dir` | Agent 在本机写入受管附属文件的目录,当前包含证书与 Lua 观测脚本 | 否 | 默认为 `data_dir` 下托管 support 目录 | `./data/etc/nginx/support` | -| `openresty_support_dir` | OpenResty 实际读取受管附属文件的目录 | 否 | 本机模式默认等于 `support_dir`;Docker 模式默认 `/etc/nginx/atsflare-support` | `/usr/local/openresty/nginx/conf/support` | +| `cert_dir` | Agent 在本机写入受管证书文件的目录 | 否 | 默认为 `data_dir` 下托管 certs 目录 | `./data/etc/nginx/certs` | +| `openresty_cert_dir` | OpenResty 实际读取受管证书文件的目录 | 否 | 本机模式默认等于 `cert_dir`;Docker 模式默认 `/etc/nginx/atsflare-certs` | `/usr/local/openresty/nginx/conf/certs` | +| `lua_dir` | Agent 在本机写入受管 Lua 观测脚本的目录;每次启动都会覆盖释放 | 否 | 默认为 `data_dir` 下托管 lua 目录 | `./data/etc/nginx/lua` | +| `openresty_lua_dir` | OpenResty 实际读取受管 Lua 观测脚本的目录 | 否 | 本机模式默认等于 `lua_dir`;Docker 模式默认 `/etc/nginx/atsflare-lua` | `/usr/local/openresty/nginx/conf/lua` | | `observability_buffer_path` | Agent 本地观测补报缓冲文件路径;用于在 server 短暂离线时按时间窗口落盘待补传数据 | 否 | 默认为 `data_dir` 下托管观测缓冲文件 | `./data/var/lib/atsflare/observability-buffer.json` | | `observability_replay_minutes` | Agent 恢复心跳后允许批量补传的最近观测窗口时长(分钟) | 否 | `15` | `30` | | `state_path` | Agent 本地状态文件路径 | 否 | 默认为 `data_dir` 下托管状态文件 | `./data/agent-state.json` | @@ -306,7 +310,6 @@ go run ./cmd/agent -config ./agent.json * 未配置 `openresty_path` 时,默认为 Docker OpenResty 模式 * `openresty_observability_port` 默认仅绑定本地回环地址;若节点本机已有端口冲突,可改为其他未占用端口 * `observability_replay_minutes` 只控制“允许补传最近多少分钟的窗口”;超出该窗口的历史观测会在本地自动裁剪 -* 为兼容旧节点,Agent 仍可读取历史字段 `cert_dir` / `openresty_cert_dir`,但保存配置时会统一写回 `support_dir` / `openresty_support_dir` * 配置保存时,`agent_version`、`nginx_version` 由程序运行时维护,不需要写入 JSON * 第五版主配置接管完成后,本机模式下应优先通过 `main_config_path` 由 Agent 写入受管主配置,而不是依赖节点手工维护 include 规则 @@ -318,7 +321,8 @@ go run ./cmd/agent -config ./agent.json | --- | --- | | `main_config_path` | 第五版 Docker 模式默认可落在 `data_dir/etc/nginx/nginx.conf`;本机模式建议显式配置 | | `route_config_path` | `data_dir/etc/nginx/conf.d/atsflare_routes.conf` | -| `support_dir` | `data_dir/etc/nginx/support` | +| `cert_dir` | `data_dir/etc/nginx/certs` | +| `lua_dir` | `data_dir/etc/nginx/lua` | | `observability_buffer_path` | `data_dir/var/lib/atsflare/observability-buffer.json` | | `state_path` | `data_dir/var/lib/atsflare/agent-state.json` | @@ -326,11 +330,13 @@ Docker OpenResty 模式下: | 字段 | 默认值 | | --- | --- | -| `openresty_support_dir` | `/etc/nginx/atsflare-support` | +| `openresty_cert_dir` | `/etc/nginx/atsflare-certs` | +| `openresty_lua_dir` | `/etc/nginx/atsflare-lua` | 补充说明: -* Agent 当前会随受管配置一并向 OpenResty 注入 Lua 观测脚本,并在每次 heartbeat 前通过 `http://127.0.0.1:/atsflare/observability` 读取最近窗口请求指标 +* Agent 会在每次启动时把受管 Lua 观测脚本覆盖释放到 `lua_dir`,不再由 Server 随配置版本下发 +* OpenResty 通过 `openresty_lua_dir` 读取这些本地脚本,并在每次 heartbeat 前通过 `http://127.0.0.1:/atsflare/observability` 读取最近窗口请求指标 * 若 server 短暂离线,Agent 会把最近窗口观测先写入 `observability_buffer_path`,待 heartbeat 恢复后按时间窗口批量补传最近 `observability_replay_minutes` 分钟的数据 * 同一端口还会暴露仅本机可访问的 `stub_status`,用于采集 OpenResty 活动连接数 @@ -359,9 +365,11 @@ Docker OpenResty 模式下: "openresty_path": "/usr/local/openresty/nginx/sbin/openresty", "main_config_path": "/usr/local/openresty/nginx/conf/nginx.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf", - "support_dir": "/usr/local/openresty/nginx/conf/support", - "openresty_support_dir": "/usr/local/openresty/nginx/conf/support" -} + "cert_dir": "/usr/local/openresty/nginx/conf/certs", + "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs", + "lua_dir": "/usr/local/openresty/nginx/conf/lua", + "openresty_lua_dir": "/usr/local/openresty/nginx/conf/lua" +} ``` --- diff --git a/docs/deployment.md b/docs/deployment.md index a2e31c71..b03b50c9 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -285,8 +285,8 @@ export LOG_LEVEL='info' 验证点: -1. 首次启动后确认 `data/etc/nginx/nginx.conf`、`data/etc/nginx/conf.d/atsflare_routes.conf` 与 `data/etc/nginx/support` 已由 Agent 创建 -2. 确认容器实际挂载了主配置、路由目录和证书目录 +1. 首次启动后确认 `data/etc/nginx/nginx.conf`、`data/etc/nginx/conf.d/atsflare_routes.conf`、`data/etc/nginx/certs` 与 `data/etc/nginx/lua` 已由 Agent 创建 +2. 确认容器实际挂载了主配置、路由目录、证书目录和 Lua 目录 3. 确认宿主机本地可访问 `http://127.0.0.1:18081/atsflare/observability` 与 `http://127.0.0.1:18081/atsflare/stub_status` 3. 在管理端发布一次新版本后,确认节点 `current_version` 追平激活版本 4. 在节点详情查看“当前目标版本”与“最近应用”,确认主配置/路由配置快照和 checksum 已可见 @@ -300,7 +300,7 @@ docker exec atsflare-openresty openresty -t 说明: -* `docker inspect` 重点确认主配置文件、`conf.d` 目录和证书目录都来自 Agent 受管路径 +* `docker inspect` 重点确认主配置文件、`conf.d` 目录、证书目录和 Lua 目录都来自 Agent 受管路径 * 观测端口默认只绑定 `127.0.0.1`;若节点已有冲突,可在 `agent.json` 中调整 `openresty_observability_port` * 若容器名使用默认值,请将上述命令中的名称替换为 `atsflare-openresty` @@ -315,8 +315,10 @@ docker exec atsflare-openresty openresty -t "openresty_path": "/usr/local/openresty/nginx/sbin/openresty", "main_config_path": "/usr/local/openresty/nginx/conf/nginx.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf", - "support_dir": "/usr/local/openresty/nginx/conf/support", - "openresty_support_dir": "/usr/local/openresty/nginx/conf/support", + "cert_dir": "/usr/local/openresty/nginx/conf/certs", + "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs", + "lua_dir": "/usr/local/openresty/nginx/conf/lua", + "openresty_lua_dir": "/usr/local/openresty/nginx/conf/lua", "openresty_observability_port": 18081 } ```