mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 16:46:37 +08:00
[优化] 改名
This commit is contained in:
@@ -0,0 +1,803 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"openflare-agent/internal/protocol"
|
||||
)
|
||||
|
||||
const CertDirPlaceholder = "__OPENFLARE_CERT_DIR__"
|
||||
const RouteConfigPlaceholder = "__OPENFLARE_ROUTE_CONFIG__"
|
||||
const AccessLogPlaceholder = "__OPENFLARE_ACCESS_LOG__"
|
||||
const LuaDirPlaceholder = "__OPENFLARE_LUA_DIR__"
|
||||
const ObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__"
|
||||
const ObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__"
|
||||
const DockerMainConfigPath = "/usr/local/openresty/nginx/conf/nginx.conf"
|
||||
const DockerRouteConfigPath = "/etc/nginx/conf.d/openflare_routes.conf"
|
||||
const DockerAccessLogPath = "/etc/nginx/conf.d/openflare_access.log"
|
||||
|
||||
const dockerRuntimeCommand = "openresty"
|
||||
|
||||
type Executor interface {
|
||||
Test(ctx context.Context) error
|
||||
Reload(ctx context.Context) error
|
||||
EnsureRuntime(ctx context.Context, recreate bool) error
|
||||
CheckHealth(ctx context.Context) error
|
||||
Restart(ctx context.Context) error
|
||||
}
|
||||
|
||||
type CommandRunner interface {
|
||||
Run(ctx context.Context, name string, args ...string) ([]byte, error)
|
||||
}
|
||||
|
||||
type OSCommandRunner struct{}
|
||||
|
||||
func (r *OSCommandRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
return output, err
|
||||
}
|
||||
|
||||
type PathExecutor struct {
|
||||
Path string
|
||||
Runner CommandRunner
|
||||
}
|
||||
|
||||
func (e *PathExecutor) Test(ctx context.Context) error {
|
||||
slog.Debug("running openresty test with binary", "path", e.Path)
|
||||
output, err := e.Runner.Run(ctx, e.Path, "-t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("openresty -t failed: %w: %s", err, string(output))
|
||||
}
|
||||
slog.Debug("openresty test succeeded with binary", "path", e.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *PathExecutor) Reload(ctx context.Context) error {
|
||||
slog.Debug("running openresty reload with binary", "path", e.Path)
|
||||
output, err := e.Runner.Run(ctx, e.Path, "-s", "reload")
|
||||
if err != nil {
|
||||
return fmt.Errorf("openresty reload failed: %w: %s", err, string(output))
|
||||
}
|
||||
slog.Debug("openresty reload succeeded with binary", "path", e.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *PathExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *PathExecutor) CheckHealth(ctx context.Context) error {
|
||||
return e.Test(ctx)
|
||||
}
|
||||
|
||||
func (e *PathExecutor) Restart(ctx context.Context) error {
|
||||
slog.Info("restarting openresty with binary", "path", e.Path)
|
||||
output, err := e.Runner.Run(ctx, e.Path, "-s", "quit")
|
||||
if err != nil {
|
||||
text := string(output)
|
||||
if !isIgnorableOpenrestyStopError(text) {
|
||||
return fmt.Errorf("openresty stop failed: %w: %s", err, text)
|
||||
}
|
||||
}
|
||||
output, err = e.Runner.Run(ctx, e.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("openresty start failed: %w: %s", err, string(output))
|
||||
}
|
||||
slog.Info("openresty restart succeeded with binary", "path", e.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
type DockerExecutor struct {
|
||||
DockerBinary string
|
||||
ContainerName string
|
||||
Image string
|
||||
MainConfigPath string
|
||||
RouteConfigDir string
|
||||
CertDir string
|
||||
NginxCertDir string
|
||||
LuaDir string
|
||||
NginxLuaDir string
|
||||
OpenrestyObservabilityPort int
|
||||
Runner CommandRunner
|
||||
}
|
||||
|
||||
func (e *DockerExecutor) Test(ctx context.Context) error {
|
||||
slog.Debug("running docker openresty test", "container", e.ContainerName, "image", e.Image)
|
||||
output, err := e.runEphemeralRuntimeCommand(ctx, "-t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("docker %s -t failed: %w: %s", dockerRuntimeCommand, err, string(output))
|
||||
}
|
||||
slog.Debug("docker openresty test succeeded", "container", e.ContainerName, "runtime", dockerRuntimeCommand)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *DockerExecutor) Reload(ctx context.Context) error {
|
||||
return e.EnsureRuntime(ctx, true)
|
||||
}
|
||||
|
||||
func (e *DockerExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
|
||||
slog.Info("ensuring docker openresty runtime", "container", e.ContainerName, "recreate", recreate)
|
||||
output, err := e.Runner.Run(ctx, e.DockerBinary, "inspect", "-f", "{{.State.Running}}", e.ContainerName)
|
||||
if err == nil {
|
||||
if recreate {
|
||||
if err := e.removeContainer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.runContainer(ctx)
|
||||
}
|
||||
if strings.TrimSpace(string(output)) == "true" {
|
||||
slog.Debug("docker openresty runtime already healthy", "container", e.ContainerName)
|
||||
return nil
|
||||
}
|
||||
if err := e.removeContainer(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.runContainer(ctx)
|
||||
}
|
||||
return e.runContainer(ctx)
|
||||
}
|
||||
|
||||
func (e *DockerExecutor) CheckHealth(ctx context.Context) error {
|
||||
slog.Debug("checking docker openresty runtime health", "container", e.ContainerName)
|
||||
output, err := e.Runner.Run(ctx, e.DockerBinary, "inspect", "-f", "{{.State.Running}}", e.ContainerName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("docker inspect openresty failed: %w: %s", err, string(output))
|
||||
}
|
||||
if strings.TrimSpace(string(output)) != "true" {
|
||||
return errors.New("docker openresty container is not running")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *DockerExecutor) Restart(ctx context.Context) error {
|
||||
return e.EnsureRuntime(ctx, true)
|
||||
}
|
||||
|
||||
func (e *DockerExecutor) removeContainer(ctx context.Context) error {
|
||||
slog.Info("removing docker openresty container", "container", e.ContainerName)
|
||||
output, err := e.Runner.Run(ctx, e.DockerBinary, "rm", "-f", e.ContainerName)
|
||||
if err != nil {
|
||||
text := string(output)
|
||||
if strings.Contains(text, "No such container") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("docker rm openresty failed: %w: %s", err, text)
|
||||
}
|
||||
slog.Info("docker openresty container removed", "container", e.ContainerName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *DockerExecutor) runContainer(ctx context.Context) error {
|
||||
slog.Info("starting docker openresty container", "container", e.ContainerName, "image", e.Image)
|
||||
runArgs := []string{
|
||||
"run", "-d",
|
||||
"--name", e.ContainerName,
|
||||
"-p", "80:80",
|
||||
"-p", "443:443",
|
||||
"-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.CertDir, e.NginxCertDir),
|
||||
"-v", fmt.Sprintf("%s:%s", e.LuaDir, e.NginxLuaDir),
|
||||
e.Image,
|
||||
}
|
||||
runOutput, runErr := e.Runner.Run(ctx, e.DockerBinary, runArgs...)
|
||||
if runErr != nil {
|
||||
return fmt.Errorf("docker run openresty failed: %w: %s", runErr, string(runOutput))
|
||||
}
|
||||
slog.Info("docker openresty container started", "container", e.ContainerName)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
MainConfigPath string
|
||||
RouteConfigPath string
|
||||
RuntimeRouteConfigPath 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, "cert_files", len(supportFiles))
|
||||
backup, err := m.backup()
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
renderedMainConfig := m.renderMainConfig(mainConfig)
|
||||
if err = os.WriteFile(m.MainConfigPath, []byte(renderedMainConfig), 0o644); err != nil {
|
||||
slog.Error("writing openresty main config failed, restoring backup", "error", err)
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
renderedRouteConfig := m.renderRouteConfig(routeConfig)
|
||||
if err = os.WriteFile(m.RouteConfigPath, []byte(renderedRouteConfig), 0o644); err != nil {
|
||||
slog.Error("writing openresty route config failed, restoring backup", "error", err)
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
if err = m.Executor.Test(ctx); err != nil {
|
||||
slog.Error("openresty test failed after config write, restoring backup", "error", err)
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
if err = m.Executor.Reload(ctx); err != nil {
|
||||
slog.Error("openresty reload failed after config write, restoring backup", "error", err)
|
||||
_ = m.restore(backup)
|
||||
return err
|
||||
}
|
||||
slog.Info("openresty apply completed successfully", "main_config", m.MainConfigPath, "route_config", m.RouteConfigPath)
|
||||
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 未配置")
|
||||
}
|
||||
slog.Info("openresty ensure runtime requested", "recreate", recreate)
|
||||
return m.Executor.EnsureRuntime(ctx, recreate)
|
||||
}
|
||||
|
||||
func (m *Manager) CheckHealth(ctx context.Context) error {
|
||||
if m.Executor == nil {
|
||||
return errors.New("executor 未配置")
|
||||
}
|
||||
return m.Executor.CheckHealth(ctx)
|
||||
}
|
||||
|
||||
func (m *Manager) Restart(ctx context.Context) error {
|
||||
if m.Executor == nil {
|
||||
return errors.New("executor 未配置")
|
||||
}
|
||||
slog.Info("openresty restart requested")
|
||||
return m.Executor.Restart(ctx)
|
||||
}
|
||||
|
||||
func (m *Manager) CurrentChecksum() (string, error) {
|
||||
if m.RouteConfigPath == "" {
|
||||
return "", errors.New("route config path 不能为空")
|
||||
}
|
||||
if m.MainConfigPath == "" {
|
||||
return "", errors.New("main config path 不能为空")
|
||||
}
|
||||
mainData, err := os.ReadFile(m.MainConfigPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
data, err := os.ReadFile(m.RouteConfigPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
normalizedMain := string(mainData)
|
||||
if includePath := m.routeConfigIncludePath(); includePath != "" {
|
||||
normalizedMain = strings.ReplaceAll(normalizedMain, includePath, RouteConfigPlaceholder)
|
||||
}
|
||||
if accessLogPath := m.accessLogRuntimePath(); accessLogPath != "" {
|
||||
normalizedMain = strings.ReplaceAll(normalizedMain, accessLogPath, AccessLogPlaceholder)
|
||||
}
|
||||
if luaDir := m.luaRuntimePath(); luaDir != "" {
|
||||
normalizedMain = strings.ReplaceAll(normalizedMain, luaDir, LuaDirPlaceholder)
|
||||
}
|
||||
if listen := strings.TrimSpace(m.OpenrestyObservabilityListen); listen != "" {
|
||||
normalizedMain = strings.ReplaceAll(normalizedMain, listen, ObservabilityListenPlaceholder)
|
||||
}
|
||||
if m.OpenrestyObservabilityPort > 0 {
|
||||
normalizedMain = strings.ReplaceAll(normalizedMain, fmt.Sprintf("%d", m.OpenrestyObservabilityPort), ObservabilityPortPlaceholder)
|
||||
}
|
||||
normalizedRoute := string(data)
|
||||
if m.NginxCertDir != "" {
|
||||
normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxCertDir, CertDirPlaceholder)
|
||||
}
|
||||
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, "cert_files", len(files))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type ExecutorOptions struct {
|
||||
NginxPath string
|
||||
DockerBinary string
|
||||
ContainerName string
|
||||
Image string
|
||||
MainConfigPath string
|
||||
RouteConfigPath string
|
||||
CertDir string
|
||||
NginxCertDir string
|
||||
LuaDir string
|
||||
NginxLuaDir string
|
||||
OpenrestyObservabilityPort int
|
||||
}
|
||||
|
||||
func NewExecutor(options ExecutorOptions) Executor {
|
||||
runner := &OSCommandRunner{}
|
||||
if options.NginxPath != "" {
|
||||
return &PathExecutor{
|
||||
Path: options.NginxPath,
|
||||
Runner: runner,
|
||||
}
|
||||
}
|
||||
mainConfigPath := options.MainConfigPath
|
||||
if mainConfigPath != "" {
|
||||
if absPath, err := filepath.Abs(mainConfigPath); err == nil {
|
||||
mainConfigPath = absPath
|
||||
}
|
||||
}
|
||||
routeConfigDir := filepath.Dir(options.RouteConfigPath)
|
||||
if options.RouteConfigPath != "" {
|
||||
if absDir, err := filepath.Abs(routeConfigDir); err == nil {
|
||||
routeConfigDir = 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,
|
||||
ContainerName: options.ContainerName,
|
||||
Image: options.Image,
|
||||
MainConfigPath: mainConfigPath,
|
||||
RouteConfigDir: routeConfigDir,
|
||||
CertDir: certDir,
|
||||
NginxCertDir: options.NginxCertDir,
|
||||
LuaDir: luaDir,
|
||||
NginxLuaDir: options.NginxLuaDir,
|
||||
OpenrestyObservabilityPort: options.OpenrestyObservabilityPort,
|
||||
Runner: runner,
|
||||
}
|
||||
}
|
||||
|
||||
func DetectVersion(ctx context.Context, options ExecutorOptions) string {
|
||||
version, err := detectVersion(ctx, options, &OSCommandRunner{})
|
||||
if err != nil {
|
||||
slog.Error("detect openresty version failed", "error", err)
|
||||
return ""
|
||||
}
|
||||
slog.Info("detected openresty version", "version", version)
|
||||
return version
|
||||
}
|
||||
|
||||
func detectVersion(ctx context.Context, options ExecutorOptions, runner CommandRunner) (string, error) {
|
||||
if runner == nil {
|
||||
runner = &OSCommandRunner{}
|
||||
}
|
||||
if options.NginxPath != "" {
|
||||
output, err := runner.Run(ctx, options.NginxPath, "-v")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("run runtime -v failed: %w: %s", err, string(output))
|
||||
}
|
||||
version := parseNginxVersion(string(output))
|
||||
if version == "" {
|
||||
return "", errors.New("cannot parse runtime version from binary output")
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
output, err := runDockerVersionProbe(ctx, runner, options.DockerBinary, options.Image)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("run docker %s -v failed: %w: %s", dockerRuntimeCommand, err, string(output))
|
||||
}
|
||||
version := parseNginxVersion(string(output))
|
||||
if version == "" {
|
||||
return "", errors.New("cannot parse runtime version from docker output")
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func parseNginxVersion(output string) string {
|
||||
matches := nginxVersionPattern.FindStringSubmatch(output)
|
||||
if len(matches) != 2 {
|
||||
return ""
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
var nginxVersionPattern = regexp.MustCompile(`(?im)(?:nginx|openresty) version:\s*(?:nginx|openresty)/([^\s]+)`)
|
||||
|
||||
func isIgnorableOpenrestyStopError(output string) bool {
|
||||
text := strings.ToLower(strings.TrimSpace(output))
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(text, "invalid pid") || strings.Contains(text, "no such process")
|
||||
}
|
||||
|
||||
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:%s", e.MainConfigPath, DockerMainConfigPath),
|
||||
"-v",
|
||||
fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir),
|
||||
"-v",
|
||||
fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir),
|
||||
"-v",
|
||||
fmt.Sprintf("%s:%s", e.LuaDir, e.NginxLuaDir),
|
||||
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 {
|
||||
MainExisted bool
|
||||
MainData []byte
|
||||
RouteExisted bool
|
||||
RouteData []byte
|
||||
Files []protocol.SupportFile
|
||||
}
|
||||
|
||||
func (m *Manager) backup() (*backupState, error) {
|
||||
if m.MainConfigPath == "" {
|
||||
return nil, errors.New("main config path 不能为空")
|
||||
}
|
||||
if m.RouteConfigPath == "" {
|
||||
return nil, errors.New("route config path 不能为空")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(m.MainConfigPath), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.CertDir != "" {
|
||||
if err := os.MkdirAll(m.CertDir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
state := &backupState{}
|
||||
mainData, err := os.ReadFile(m.MainConfigPath)
|
||||
if err == nil {
|
||||
state.MainExisted = true
|
||||
state.MainData = mainData
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(m.RouteConfigPath)
|
||||
if err == nil {
|
||||
state.RouteExisted = true
|
||||
state.RouteData = data
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
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, "cert_files", len(state.Files))
|
||||
return state, nil
|
||||
}
|
||||
|
||||
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, "cert_files", len(state.Files))
|
||||
if state.MainExisted {
|
||||
if err := os.WriteFile(m.MainConfigPath, state.MainData, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := os.Remove(m.MainConfigPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if state.RouteExisted {
|
||||
if err := os.WriteFile(m.RouteConfigPath, state.RouteData, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := os.Remove(m.RouteConfigPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if m.CertDir == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(m.CertDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range state.Files {
|
||||
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), certFileMode(file.Path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error {
|
||||
if m.CertDir == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(m.CertDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
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), certFileMode(file.Path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) {
|
||||
if m.CertDir == "" {
|
||||
return nil, 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.CertDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relativePath, err := filepath.Rel(m.CertDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, protocol.SupportFile{
|
||||
Path: filepath.ToSlash(relativePath),
|
||||
Content: string(data),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Slice(files, func(i int, j int) bool {
|
||||
return files[i].Path < files[j].Path
|
||||
})
|
||||
return files, nil
|
||||
}
|
||||
|
||||
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, `\`) {
|
||||
candidate = strings.ReplaceAll(candidate, `\`, "/")
|
||||
}
|
||||
normalizedPath := filepath.Clean(filepath.FromSlash(candidate))
|
||||
if normalizedPath == "." || normalizedPath == "" {
|
||||
return "", errors.New("cert file path 不能为空")
|
||||
}
|
||||
if filepath.IsAbs(normalizedPath) || filepath.VolumeName(normalizedPath) != "" {
|
||||
return "", fmt.Errorf("cert file path %q must be relative", relativePath)
|
||||
}
|
||||
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("cert file path %q escapes cert dir", relativePath)
|
||||
}
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
func certFileMode(relativePath string) fs.FileMode {
|
||||
switch strings.ToLower(filepath.Ext(strings.TrimSpace(relativePath))) {
|
||||
case ".crt", ".pem":
|
||||
return 0o644
|
||||
case ".key":
|
||||
return 0o600
|
||||
default:
|
||||
return 0o644
|
||||
}
|
||||
}
|
||||
|
||||
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.NginxCertDir == "" {
|
||||
return content
|
||||
}
|
||||
return strings.ReplaceAll(content, CertDirPlaceholder, m.NginxCertDir)
|
||||
}
|
||||
|
||||
func (m *Manager) renderMainConfig(content string) string {
|
||||
rendered := content
|
||||
if includePath := m.routeConfigIncludePath(); includePath != "" {
|
||||
rendered = strings.ReplaceAll(rendered, RouteConfigPlaceholder, includePath)
|
||||
}
|
||||
if accessLogPath := m.accessLogRuntimePath(); accessLogPath != "" {
|
||||
rendered = strings.ReplaceAll(rendered, AccessLogPlaceholder, accessLogPath)
|
||||
}
|
||||
if luaDir := m.luaRuntimePath(); luaDir != "" {
|
||||
rendered = strings.ReplaceAll(rendered, LuaDirPlaceholder, luaDir)
|
||||
}
|
||||
if listen := strings.TrimSpace(m.OpenrestyObservabilityListen); listen != "" {
|
||||
rendered = strings.ReplaceAll(rendered, ObservabilityListenPlaceholder, listen)
|
||||
}
|
||||
if m.OpenrestyObservabilityPort > 0 {
|
||||
rendered = strings.ReplaceAll(rendered, ObservabilityPortPlaceholder, fmt.Sprintf("%d", m.OpenrestyObservabilityPort))
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
func ObservabilityListenAddress(openrestyPath string, port int) string {
|
||||
if port <= 0 {
|
||||
return ""
|
||||
}
|
||||
if strings.TrimSpace(openrestyPath) != "" {
|
||||
return fmt.Sprintf("127.0.0.1:%d", port)
|
||||
}
|
||||
return fmt.Sprintf("%d", port)
|
||||
}
|
||||
|
||||
func (m *Manager) routeConfigIncludePath() string {
|
||||
if strings.TrimSpace(m.RuntimeRouteConfigPath) != "" {
|
||||
return strings.TrimSpace(m.RuntimeRouteConfigPath)
|
||||
}
|
||||
return strings.TrimSpace(m.RouteConfigPath)
|
||||
}
|
||||
|
||||
func (m *Manager) accessLogRuntimePath() string {
|
||||
includePath := m.routeConfigIncludePath()
|
||||
if strings.TrimSpace(includePath) == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.ToSlash(filepath.Join(filepath.Dir(includePath), "openflare_access.log"))
|
||||
}
|
||||
|
||||
func (m *Manager) luaRuntimePath() string {
|
||||
if strings.TrimSpace(m.NginxLuaDir) == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.ToSlash(m.NginxLuaDir)
|
||||
}
|
||||
|
||||
func checksum(content string) string {
|
||||
sum := sha256.Sum256([]byte(content))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func bundleChecksum(mainConfig string, routeConfig string, supportFiles []protocol.SupportFile) string {
|
||||
files := append([]protocol.SupportFile(nil), supportFiles...)
|
||||
sort.Slice(files, func(i int, j int) bool {
|
||||
return files[i].Path < files[j].Path
|
||||
})
|
||||
var builder strings.Builder
|
||||
builder.WriteString(mainConfig)
|
||||
builder.WriteString("\n--route-config--\n")
|
||||
builder.WriteString(routeConfig)
|
||||
builder.WriteString("\n--support-files--\n")
|
||||
for _, file := range files {
|
||||
builder.WriteString(file.Path)
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(file.Content)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
return checksum(builder.String())
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"openflare-agent/internal/protocol"
|
||||
)
|
||||
|
||||
type runCall struct {
|
||||
name string
|
||||
args []string
|
||||
}
|
||||
|
||||
type fakeRunner struct {
|
||||
calls []runCall
|
||||
runFn func(name string, args ...string) ([]byte, error)
|
||||
}
|
||||
|
||||
type fakeExecutor struct {
|
||||
testErr error
|
||||
reloadErr error
|
||||
}
|
||||
|
||||
func (r *fakeRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
r.calls = append(r.calls, runCall{name: name, args: append([]string{}, args...)})
|
||||
if r.runFn != nil {
|
||||
return r.runFn(name, args...)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) Test(ctx context.Context) error {
|
||||
return e.testErr
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) Reload(ctx context.Context) error {
|
||||
return e.reloadErr
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) CheckHealth(ctx context.Context) error {
|
||||
return e.testErr
|
||||
}
|
||||
|
||||
func (e *fakeExecutor) Restart(ctx context.Context) error {
|
||||
return e.reloadErr
|
||||
}
|
||||
|
||||
func TestPathExecutorCommands(t *testing.T) {
|
||||
runner := &fakeRunner{}
|
||||
executor := &PathExecutor{
|
||||
Path: "/usr/local/openresty/nginx/sbin/openresty",
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
if err := executor.Test(context.Background()); err != nil {
|
||||
t.Fatalf("Test failed: %v", err)
|
||||
}
|
||||
if err := executor.Reload(context.Background()); err != nil {
|
||||
t.Fatalf("Reload failed: %v", err)
|
||||
}
|
||||
|
||||
expected := []runCall{
|
||||
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-t"}},
|
||||
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload"}},
|
||||
}
|
||||
if !reflect.DeepEqual(runner.calls, expected) {
|
||||
t.Fatalf("unexpected calls: %#v", runner.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExecutorEnsureRuntimeNoop(t *testing.T) {
|
||||
executor := &PathExecutor{
|
||||
Path: "/usr/local/openresty/nginx/sbin/openresty",
|
||||
Runner: &fakeRunner{},
|
||||
}
|
||||
if err := executor.EnsureRuntime(context.Background(), true); err != nil {
|
||||
t.Fatalf("EnsureRuntime failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) {
|
||||
runner := &fakeRunner{
|
||||
runFn: func(name string, args ...string) ([]byte, error) {
|
||||
if len(args) == 2 && args[0] == "-s" && args[1] == "quit" {
|
||||
return []byte("openresty: [error] invalid PID number \"\" in \"/usr/local/openresty/nginx/logs/nginx.pid\""), errors.New("exit status 1")
|
||||
}
|
||||
return []byte(""), nil
|
||||
},
|
||||
}
|
||||
executor := &PathExecutor{
|
||||
Path: "/usr/local/openresty/nginx/sbin/openresty",
|
||||
Runner: runner,
|
||||
}
|
||||
if err := executor.Restart(context.Background()); err != nil {
|
||||
t.Fatalf("Restart failed: %v", err)
|
||||
}
|
||||
if len(runner.calls) != 2 {
|
||||
t.Fatalf("expected 2 restart calls, got %d", len(runner.calls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerExecutorCheckHealthFailsWhenContainerStopped(t *testing.T) {
|
||||
runner := &fakeRunner{
|
||||
runFn: func(name string, args ...string) ([]byte, error) {
|
||||
return []byte("false"), nil
|
||||
},
|
||||
}
|
||||
executor := &DockerExecutor{
|
||||
DockerBinary: "docker",
|
||||
ContainerName: "openflare-openresty",
|
||||
Image: "openresty/openresty:alpine",
|
||||
MainConfigPath: filepath.Clean("/tmp/nginx.conf"),
|
||||
RouteConfigDir: filepath.Clean("/tmp/routes"),
|
||||
CertDir: filepath.Clean("/tmp/certs"),
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Clean("/tmp/lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
Runner: runner,
|
||||
}
|
||||
if err := executor.CheckHealth(context.Background()); err == nil {
|
||||
t.Fatal("expected CheckHealth to fail when container is not running")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerExecutorStartsContainerWhenMissing(t *testing.T) {
|
||||
runner := &fakeRunner{
|
||||
runFn: func(name string, args ...string) ([]byte, error) {
|
||||
if len(args) >= 1 && args[0] == "inspect" {
|
||||
return []byte(""), errors.New("not found")
|
||||
}
|
||||
return []byte("ok"), nil
|
||||
},
|
||||
}
|
||||
executor := &DockerExecutor{
|
||||
DockerBinary: "docker",
|
||||
ContainerName: "openflare-openresty",
|
||||
Image: "openresty/openresty:alpine",
|
||||
MainConfigPath: filepath.Clean("/tmp/nginx.conf"),
|
||||
RouteConfigDir: filepath.Clean("/tmp/routes"),
|
||||
CertDir: filepath.Clean("/tmp/certs"),
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Clean("/tmp/lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
if err := executor.Test(context.Background()); err != nil {
|
||||
t.Fatalf("Test failed: %v", err)
|
||||
}
|
||||
|
||||
if len(runner.calls) != 1 {
|
||||
t.Fatalf("expected 1 call, got %d", len(runner.calls))
|
||||
}
|
||||
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])
|
||||
}
|
||||
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) {
|
||||
runner := &fakeRunner{
|
||||
runFn: func(name string, args ...string) ([]byte, error) {
|
||||
if len(args) >= 2 && args[0] == "inspect" {
|
||||
return []byte("false"), nil
|
||||
}
|
||||
return []byte("ok"), nil
|
||||
},
|
||||
}
|
||||
executor := &DockerExecutor{
|
||||
DockerBinary: "docker",
|
||||
ContainerName: "openflare-openresty",
|
||||
Image: "openresty/openresty:alpine",
|
||||
MainConfigPath: filepath.Clean("/tmp/nginx.conf"),
|
||||
RouteConfigDir: filepath.Clean("/tmp/routes"),
|
||||
CertDir: filepath.Clean("/tmp/certs"),
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Clean("/tmp/lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
if err := executor.Reload(context.Background()); err != nil {
|
||||
t.Fatalf("Reload failed: %v", err)
|
||||
}
|
||||
|
||||
if len(runner.calls) != 3 {
|
||||
t.Fatalf("expected 3 calls, got %d", len(runner.calls))
|
||||
}
|
||||
if runner.calls[0].args[0] != "inspect" {
|
||||
t.Fatalf("expected docker inspect on first call, got %#v", runner.calls[0])
|
||||
}
|
||||
if runner.calls[1].args[0] != "rm" {
|
||||
t.Fatalf("expected docker rm on second call, got %#v", runner.calls[1])
|
||||
}
|
||||
if runner.calls[2].args[0] != "run" {
|
||||
t.Fatalf("expected docker run on third call, got %#v", runner.calls[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) {
|
||||
mainConfigPath := filepath.Clean("/tmp/managed/nginx.conf")
|
||||
routeConfigDir := filepath.Clean("/tmp/managed/conf.d")
|
||||
certDir := filepath.Clean("/tmp/managed/certs")
|
||||
luaDir := filepath.Clean("/tmp/managed/lua")
|
||||
runner := &fakeRunner{}
|
||||
executor := &DockerExecutor{
|
||||
DockerBinary: "docker",
|
||||
ContainerName: "openflare-openresty",
|
||||
Image: "openresty/openresty:alpine",
|
||||
MainConfigPath: mainConfigPath,
|
||||
RouteConfigDir: routeConfigDir,
|
||||
CertDir: certDir,
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: luaDir,
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
OpenrestyObservabilityPort: 18081,
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
if err := executor.runContainer(context.Background()); err != nil {
|
||||
t.Fatalf("runContainer failed: %v", err)
|
||||
}
|
||||
|
||||
if len(runner.calls) != 1 {
|
||||
t.Fatalf("expected one docker run call, got %d", len(runner.calls))
|
||||
}
|
||||
|
||||
expectedArgs := []string{
|
||||
"run", "-d",
|
||||
"--name", "openflare-openresty",
|
||||
"-p", "80:80",
|
||||
"-p", "443:443",
|
||||
"-p", "127.0.0.1:18081:18081",
|
||||
"-v", mainConfigPath + ":" + DockerMainConfigPath,
|
||||
"-v", routeConfigDir + ":/etc/nginx/conf.d",
|
||||
"-v", certDir + ":/etc/nginx/openflare-certs",
|
||||
"-v", luaDir + ":/etc/nginx/openflare-lua",
|
||||
"openresty/openresty:alpine",
|
||||
}
|
||||
if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) {
|
||||
t.Fatalf("unexpected docker run args: %#v", runner.calls[0].args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) {
|
||||
runner := &fakeRunner{
|
||||
runFn: func(name string, args ...string) ([]byte, error) {
|
||||
if len(args) >= 1 && args[0] == "inspect" {
|
||||
return []byte("true"), nil
|
||||
}
|
||||
return []byte("ok"), nil
|
||||
},
|
||||
}
|
||||
executor := &DockerExecutor{
|
||||
DockerBinary: "docker",
|
||||
ContainerName: "openflare-openresty",
|
||||
Image: "openresty/openresty:alpine",
|
||||
MainConfigPath: filepath.Clean("/tmp/nginx.conf"),
|
||||
RouteConfigDir: filepath.Clean("/tmp/routes"),
|
||||
CertDir: filepath.Clean("/tmp/certs"),
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Clean("/tmp/lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
OpenrestyObservabilityPort: 18081,
|
||||
Runner: runner,
|
||||
}
|
||||
|
||||
if err := executor.EnsureRuntime(context.Background(), true); err != nil {
|
||||
t.Fatalf("EnsureRuntime failed: %v", err)
|
||||
}
|
||||
if len(runner.calls) != 3 {
|
||||
t.Fatalf("expected 3 calls, got %d", len(runner.calls))
|
||||
}
|
||||
if runner.calls[1].args[0] != "rm" {
|
||||
t.Fatalf("expected docker rm on second call, got %#v", runner.calls[1])
|
||||
}
|
||||
if runner.calls[2].args[0] != "run" {
|
||||
t.Fatalf("expected docker run on third call, got %#v", runner.calls[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) {
|
||||
executor := NewExecutor(ExecutorOptions{
|
||||
DockerBinary: "docker",
|
||||
ContainerName: "openflare-openresty",
|
||||
Image: "openresty/openresty:alpine",
|
||||
MainConfigPath: "./data/etc/nginx/nginx.conf",
|
||||
RouteConfigPath: "./data/etc/nginx/conf.d/openflare_routes.conf",
|
||||
CertDir: "./data/etc/nginx/certs",
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: "./data/etc/nginx/lua",
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
OpenrestyObservabilityPort: 18081,
|
||||
})
|
||||
|
||||
dockerExecutor, ok := executor.(*DockerExecutor)
|
||||
if !ok {
|
||||
t.Fatal("expected docker executor")
|
||||
}
|
||||
if !filepath.IsAbs(dockerExecutor.RouteConfigDir) {
|
||||
t.Fatalf("expected absolute route config dir, got %s", dockerExecutor.RouteConfigDir)
|
||||
}
|
||||
if !filepath.IsAbs(dockerExecutor.MainConfigPath) {
|
||||
t.Fatalf("expected absolute main config path, got %s", dockerExecutor.MainConfigPath)
|
||||
}
|
||||
if !strings.HasSuffix(dockerExecutor.RouteConfigDir, filepath.Clean("data/etc/nginx/conf.d")) {
|
||||
t.Fatalf("unexpected route config dir: %s", dockerExecutor.RouteConfigDir)
|
||||
}
|
||||
if !strings.HasSuffix(dockerExecutor.MainConfigPath, filepath.Clean("data/etc/nginx/nginx.conf")) {
|
||||
t.Fatalf("unexpected main config path: %s", dockerExecutor.MainConfigPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectVersionFromBinary(t *testing.T) {
|
||||
version, err := detectVersion(context.Background(), ExecutorOptions{
|
||||
NginxPath: "/usr/local/openresty/nginx/sbin/openresty",
|
||||
}, &fakeRunner{
|
||||
runFn: func(name string, args ...string) ([]byte, error) {
|
||||
return []byte("nginx version: openresty/1.27.1.2\n"), nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("detectVersion failed: %v", err)
|
||||
}
|
||||
if version != "1.27.1.2" {
|
||||
t.Fatalf("unexpected version: %s", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
mainPath := filepath.Join(tempDir, "nginx.conf")
|
||||
routePath := filepath.Join(tempDir, "conf.d", "openflare_routes.conf")
|
||||
certDir := filepath.Join(tempDir, "certs")
|
||||
manager := &Manager{
|
||||
MainConfigPath: mainPath,
|
||||
RouteConfigPath: routePath,
|
||||
CertDir: certDir,
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Join(tempDir, "lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
Executor: &fakeExecutor{},
|
||||
}
|
||||
|
||||
err := manager.Apply(
|
||||
context.Background(),
|
||||
"include __OPENFLARE_ROUTE_CONFIG__;\naccess_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n",
|
||||
"ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;\n",
|
||||
[]protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
|
||||
mainData, err := os.ReadFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main config: %v", err)
|
||||
}
|
||||
expectedMain := "include " + routePath + ";\naccess_log " + filepath.Join(filepath.Dir(routePath), "openflare_access.log") + " openflare_json;\n"
|
||||
if string(mainData) != expectedMain {
|
||||
t.Fatalf("unexpected main config: %s", string(mainData))
|
||||
}
|
||||
|
||||
routeData, err := os.ReadFile(routePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read route config: %v", err)
|
||||
}
|
||||
if string(routeData) != "ssl_certificate /etc/nginx/openflare-certs/1.crt;\n" {
|
||||
t.Fatalf("unexpected route config: %s", string(routeData))
|
||||
}
|
||||
|
||||
value, err := manager.CurrentChecksum()
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentChecksum failed: %v", err)
|
||||
}
|
||||
expected := bundleChecksum(
|
||||
"include __OPENFLARE_ROUTE_CONFIG__;\naccess_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n",
|
||||
"ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;\n",
|
||||
[]protocol.SupportFile{{Path: "1.crt", Content: "cert"}},
|
||||
)
|
||||
if value != expected {
|
||||
t.Fatalf("unexpected checksum: got %s want %s", value, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerApplyUsesRuntimeRouteConfigPath(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
mainPath := filepath.Join(tempDir, "nginx.conf")
|
||||
routePath := filepath.Join(tempDir, "conf.d", "openflare_routes.conf")
|
||||
manager := &Manager{
|
||||
MainConfigPath: mainPath,
|
||||
RouteConfigPath: routePath,
|
||||
RuntimeRouteConfigPath: DockerRouteConfigPath,
|
||||
CertDir: filepath.Join(tempDir, "certs"),
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Join(tempDir, "lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
Executor: &fakeExecutor{},
|
||||
}
|
||||
|
||||
if err := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\naccess_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n", "server { listen 80; }\n", nil); err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
|
||||
mainData, err := os.ReadFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main config: %v", err)
|
||||
}
|
||||
expectedMain := "include " + DockerRouteConfigPath + ";\naccess_log " + DockerAccessLogPath + " openflare_json;\n"
|
||||
if string(mainData) != expectedMain {
|
||||
t.Fatalf("unexpected main config include path: %s", string(mainData))
|
||||
}
|
||||
|
||||
value, err := manager.CurrentChecksum()
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentChecksum failed: %v", err)
|
||||
}
|
||||
expected := bundleChecksum(
|
||||
"include __OPENFLARE_ROUTE_CONFIG__;\naccess_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n",
|
||||
"server { listen 80; }\n",
|
||||
nil,
|
||||
)
|
||||
if value != expected {
|
||||
t.Fatalf("unexpected checksum: got %s want %s", value, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectVersionFromDockerImage(t *testing.T) {
|
||||
runner := &fakeRunner{
|
||||
runFn: func(name string, args ...string) ([]byte, error) {
|
||||
return []byte("nginx version: openresty/1.27.1.2\n"), nil
|
||||
},
|
||||
}
|
||||
version, err := detectVersion(context.Background(), ExecutorOptions{
|
||||
DockerBinary: "docker",
|
||||
Image: "openresty/openresty:alpine",
|
||||
}, runner)
|
||||
if err != nil {
|
||||
t.Fatalf("detectVersion failed: %v", err)
|
||||
}
|
||||
if version != "1.27.1.2" {
|
||||
t.Fatalf("unexpected version: %s", version)
|
||||
}
|
||||
if len(runner.calls) != 1 {
|
||||
t.Fatalf("expected one command call, got %d", len(runner.calls))
|
||||
}
|
||||
expectedArgs := []string{"run", "--rm", "openresty/openresty:alpine", "openresty", "-v"}
|
||||
if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) {
|
||||
t.Fatalf("unexpected docker args: %#v", runner.calls[0].args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNginxVersionIgnoresDockerEntrypointPaths(t *testing.T) {
|
||||
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?)",
|
||||
"nginx version: openresty/1.27.1.2",
|
||||
}, "\n")
|
||||
|
||||
version := parseNginxVersion(output)
|
||||
if version != "1.27.1.2" {
|
||||
t.Fatalf("unexpected version: %s", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
manager := &Manager{
|
||||
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
|
||||
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
|
||||
CertDir: filepath.Join(tempDir, "certs"),
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Join(tempDir, "lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
OpenrestyObservabilityListen: "18081",
|
||||
Executor: &fakeExecutor{},
|
||||
}
|
||||
|
||||
err := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\nserver { listen __OPENFLARE_OBSERVABILITY_LISTEN__; }", "ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;", []protocol.SupportFile{
|
||||
{Path: "1.crt", Content: "cert-data"},
|
||||
{Path: "1.key", Content: "key-data"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
|
||||
routeData, err := os.ReadFile(manager.RouteConfigPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read route config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(routeData), "/etc/nginx/openflare-certs/1.crt") {
|
||||
t.Fatalf("expected placeholder replacement in route config, got %s", string(routeData))
|
||||
}
|
||||
mainData, err := os.ReadFile(manager.MainConfigPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main config: %v", err)
|
||||
}
|
||||
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.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.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 TestCertFileMode(t *testing.T) {
|
||||
testCases := []struct {
|
||||
path string
|
||||
want os.FileMode
|
||||
}{
|
||||
{path: "1.crt", want: 0o644},
|
||||
{path: "1.pem", want: 0o644},
|
||||
{path: "1.key", want: 0o600},
|
||||
{path: "misc.txt", want: 0o644},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
if got := certFileMode(testCase.path); got != testCase.want {
|
||||
t.Fatalf("unexpected mode for %s: got %o want %o", testCase.path, got, testCase.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
manager := &Manager{
|
||||
LuaDir: filepath.Join(tempDir, "lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
}
|
||||
|
||||
err := manager.EnsureLuaAssets()
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureLuaAssets failed: %v", err)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerRollbackRestoresCertFiles(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
routePath := filepath.Join(tempDir, "routes.conf")
|
||||
mainPath := filepath.Join(tempDir, "nginx.conf")
|
||||
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 {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(routePath, []byte("old-route"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile failed: %v", err)
|
||||
}
|
||||
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,
|
||||
CertDir: certDir,
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Join(tempDir, "lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
Executor: &fakeExecutor{
|
||||
testErr: errors.New("openresty test failed"),
|
||||
},
|
||||
}
|
||||
|
||||
err := manager.Apply(context.Background(), "new-main", "new-route", []protocol.SupportFile{
|
||||
{Path: "1.crt", Content: "new-cert"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected Apply to fail")
|
||||
}
|
||||
|
||||
mainData, err := os.ReadFile(mainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main config: %v", err)
|
||||
}
|
||||
if string(mainData) != "old-main" {
|
||||
t.Fatalf("expected main rollback, got %s", string(mainData))
|
||||
}
|
||||
routeData, err := os.ReadFile(routePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read route config: %v", err)
|
||||
}
|
||||
if string(routeData) != "old-route" {
|
||||
t.Fatalf("expected route rollback, got %s", string(routeData))
|
||||
}
|
||||
certData, err := os.ReadFile(filepath.Join(certDir, "1.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read cert file: %v", err)
|
||||
}
|
||||
if string(certData) != "old-cert" {
|
||||
t.Fatalf("expected cert rollback, got %s", string(certData))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
absolutePath := "/tmp/evil.crt"
|
||||
if runtime.GOOS == "windows" {
|
||||
absolutePath = `C:/tmp/evil.crt`
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
path string
|
||||
shouldErr bool
|
||||
}{
|
||||
{path: "nested/1.crt", shouldErr: false},
|
||||
{path: "../escape.crt", shouldErr: true},
|
||||
{path: "..\\escape.crt", shouldErr: true},
|
||||
{path: absolutePath, shouldErr: true},
|
||||
{path: "", shouldErr: true},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
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)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("expected path %q to be accepted: %v", testCase.path, err)
|
||||
}
|
||||
if !strings.HasPrefix(targetPath, manager.CertDir) {
|
||||
t.Fatalf("expected target path %q to stay under %q", targetPath, manager.CertDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerApplyRejectsCertFilePathTraversal(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
manager := &Manager{
|
||||
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
|
||||
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
|
||||
CertDir: filepath.Join(tempDir, "certs"),
|
||||
NginxCertDir: "/etc/nginx/openflare-certs",
|
||||
LuaDir: filepath.Join(tempDir, "lua"),
|
||||
NginxLuaDir: "/etc/nginx/openflare-lua",
|
||||
Executor: &fakeExecutor{},
|
||||
}
|
||||
|
||||
err := manager.Apply(context.Background(), "main", "route", []protocol.SupportFile{
|
||||
{Path: "../escape.crt", Content: "bad"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected Apply to reject traversal path")
|
||||
}
|
||||
|
||||
if _, statErr := os.Stat(filepath.Join(tempDir, "escape.crt")); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("expected escaped file to not exist, stat err = %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestObservabilityListenAddress(t *testing.T) {
|
||||
if got := ObservabilityListenAddress("", 18081); got != "18081" {
|
||||
t.Fatalf("unexpected docker observability listen address: %s", got)
|
||||
}
|
||||
if got := ObservabilityListenAddress("/usr/local/openresty/nginx/sbin/openresty", 18081); got != "127.0.0.1:18081" {
|
||||
t.Fatalf("unexpected path observability listen address: %s", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package nginx
|
||||
|
||||
import "openflare-agent/internal/protocol"
|
||||
|
||||
const (
|
||||
openRestyObservabilityWindowTTL = 7200
|
||||
openRestyObservabilityWindowSize = 60
|
||||
)
|
||||
|
||||
const openRestyObservabilityInitLua = `local dict = ngx.shared.openflare_observability
|
||||
if not dict then
|
||||
return
|
||||
end
|
||||
|
||||
return
|
||||
`
|
||||
|
||||
const openRestyObservabilityLogLua = `local dict = ngx.shared.openflare_observability
|
||||
if not dict then
|
||||
return
|
||||
end
|
||||
|
||||
local request_uri = tostring(ngx.var.uri or "")
|
||||
if request_uri == "/openflare/observability" or request_uri == "/openflare/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.openflare_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},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user