[新增] Agent 架构调整, 采用集成镜像方式

This commit is contained in:
ryan
2026-05-28 22:59:50 +08:00
parent c856faca50
commit 95d58eb724
15 changed files with 681 additions and 1035 deletions
+11
View File
@@ -0,0 +1,11 @@
.git
.idea
anubis-source
**/node_modules
**/.next
**/build
**/dist
**/.cache
**/coverage
**/*.db
**/*.log
+163
View File
@@ -179,3 +179,166 @@ jobs:
- name: Inspect image - name: Inspect image
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}" run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
build-agent:
name: Build Agent (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}-agent" >> "$GITHUB_ENV"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./openflare_agent/Dockerfile
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
build-args: |
VERSION=${{ env.VERSION }}
cache-from: type=gha,scope=docker-agent-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=docker-agent-${{ matrix.arch }}
- name: Export digest
shell: bash
run: |
mkdir -p /tmp/agent-digests
touch "/tmp/agent-digests/${DIGEST#sha256:}"
env:
DIGEST: ${{ steps.build.outputs.digest }}
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: agent-digests-${{ matrix.arch }}
path: /tmp/agent-digests/*
if-no-files-found: error
retention-days: 1
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
merge-agent:
name: Merge Agent multi-arch manifest
runs-on: ubuntu-24.04
needs: build-agent
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false
- name: Set image metadata
shell: bash
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
POINTED_TAG="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)"
INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}"
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}-agent" >> "$GITHUB_ENV"
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME}"
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ -n "$POINTED_TAG" ]]; then
VERSION="$POINTED_TAG"
else
echo "workflow_dispatch requires an explicit version input when HEAD is not tagged" >&2
exit 1
fi
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/agent-digests
pattern: agent-digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and push manifest list
working-directory: /tmp/agent-digests
shell: bash
run: |
shopt -s nullglob
references=()
for digest in *; do
references+=("${IMAGE}@sha256:${digest}")
done
if [ ${#references[@]} -eq 0 ]; then
echo "No digests found in /tmp/agent-digests" >&2
exit 1
fi
docker buildx imagetools create \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" \
"${references[@]}"
- name: Inspect image
run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"
+32
View File
@@ -0,0 +1,32 @@
ARG VERSION=dev
FROM golang:1.25-alpine AS builder
ARG VERSION
ARG TARGETOS=linux
ARG TARGETARCH
ENV CGO_ENABLED=0 \
GOOS=${TARGETOS} \
GOARCH=${TARGETARCH}
WORKDIR /build
COPY openflare_server ./openflare_server
COPY openflare_agent ./openflare_agent
WORKDIR /build/openflare_agent
RUN go mod download
RUN go build -trimpath -ldflags "-s -w -X 'openflare-agent/internal/config.AgentVersion=$VERSION'" -o /build/openflare-agent ./cmd/agent
FROM openresty/openresty:alpine
RUN apk add --no-cache ca-certificates tzdata \
&& mkdir -p /etc/openflare /data
ENV OPENFLARE_OPENRESTY_PATH=openresty \
OPENFLARE_DATA_DIR=/data
COPY --from=builder /build/openflare-agent /usr/local/bin/openflare-agent
EXPOSE 80 443 18081
ENTRYPOINT ["/usr/local/bin/openflare-agent"]
CMD ["-config", "/etc/openflare/agent.json"]
+1 -2
View File
@@ -2,8 +2,7 @@
"server_url": "http://127.0.0.1:3000", "server_url": "http://127.0.0.1:3000",
"agent_token": "373956188ddead1df6dd7c86cd330b73", "agent_token": "373956188ddead1df6dd7c86cd330b73",
"data_dir": "./data", "data_dir": "./data",
"openresty_container_name": "openflare-openresty", "openresty_path": "openresty",
"openresty_docker_image": "openresty/openresty:alpine",
"heartbeat_interval": 10000, "heartbeat_interval": 10000,
"request_timeout": 10000 "request_timeout": 10000
} }
+4 -11
View File
@@ -34,9 +34,6 @@ func main() {
context.Background(), context.Background(),
nginx.ExecutorOptions{ nginx.ExecutorOptions{
NginxPath: cfg.OpenrestyPath, NginxPath: cfg.OpenrestyPath,
DockerBinary: cfg.DockerBinary,
ContainerName: cfg.OpenrestyContainerName,
Image: cfg.OpenrestyDockerImage,
MainConfigPath: cfg.MainConfigPath, MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath, RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir, CertDir: cfg.CertDir,
@@ -52,33 +49,29 @@ func main() {
"ip", cfg.NodeIP, "ip", cfg.NodeIP,
"heartbeat_interval", cfg.HeartbeatInterval, "heartbeat_interval", cfg.HeartbeatInterval,
"route_config", cfg.RouteConfigPath, "route_config", cfg.RouteConfigPath,
"access_log", cfg.AccessLogPath,
"cert_dir", cfg.CertDir, "cert_dir", cfg.CertDir,
"lua_dir", cfg.LuaDir, "lua_dir", cfg.LuaDir,
"runtime_config_dir", cfg.RuntimeConfigDir,
) )
client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration()) client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration())
stateStore := state.NewStore(cfg.StatePath) stateStore := state.NewStore(cfg.StatePath)
observabilityBuffer := state.NewObservabilityBufferStore(cfg.ObservabilityBufferPath) observabilityBuffer := state.NewObservabilityBufferStore(cfg.ObservabilityBufferPath)
runtimeRouteConfigPath := cfg.RouteConfigPath
if cfg.OpenrestyPath == "" {
runtimeRouteConfigPath = nginx.DockerRouteConfigPath
}
runtimeManager := &nginx.Manager{ runtimeManager := &nginx.Manager{
MainConfigPath: cfg.MainConfigPath, MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath, RouteConfigPath: cfg.RouteConfigPath,
RuntimeRouteConfigPath: runtimeRouteConfigPath, AccessLogPath: cfg.AccessLogPath,
CertDir: cfg.CertDir, CertDir: cfg.CertDir,
NginxCertDir: cfg.OpenrestyCertDir, NginxCertDir: cfg.OpenrestyCertDir,
LuaDir: cfg.LuaDir, LuaDir: cfg.LuaDir,
NginxLuaDir: cfg.OpenrestyLuaDir, NginxLuaDir: cfg.OpenrestyLuaDir,
RuntimeConfigDir: cfg.RuntimeConfigDir,
OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyPath, cfg.OpenrestyObservabilityPort), OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyPath, cfg.OpenrestyObservabilityPort),
OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort,
OpenrestyResolverDirective: "", OpenrestyResolverDirective: "",
Executor: nginx.NewExecutor(nginx.ExecutorOptions{ Executor: nginx.NewExecutor(nginx.ExecutorOptions{
NginxPath: cfg.OpenrestyPath, NginxPath: cfg.OpenrestyPath,
DockerBinary: cfg.DockerBinary,
ContainerName: cfg.OpenrestyContainerName,
Image: cfg.OpenrestyDockerImage,
MainConfigPath: cfg.MainConfigPath, MainConfigPath: cfg.MainConfigPath,
RouteConfigPath: cfg.RouteConfigPath, RouteConfigPath: cfg.RouteConfigPath,
CertDir: cfg.CertDir, CertDir: cfg.CertDir,
@@ -302,15 +302,16 @@ func TestRunnerHeartbeatPayloadIncludesObservabilityExtensions(t *testing.T) {
NginxVersion: "1.27.1.2", NginxVersion: "1.27.1.2",
DataDir: tempDir, DataDir: tempDir,
RouteConfigPath: filepath.Join(tempDir, "conf.d", "openflare_routes.conf"), RouteConfigPath: filepath.Join(tempDir, "conf.d", "openflare_routes.conf"),
AccessLogPath: filepath.Join(tempDir, "var", "log", "openflare", "access.log"),
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
}, },
StateStore: stateStore, StateStore: stateStore,
} }
if err := os.MkdirAll(filepath.Dir(runner.Config.RouteConfigPath), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(runner.Config.AccessLogPath), 0o755); err != nil {
t.Fatalf("failed to prepare route config dir: %v", err) t.Fatalf("failed to prepare access log dir: %v", err)
} }
if err := os.WriteFile( if err := os.WriteFile(
filepath.Join(filepath.Dir(runner.Config.RouteConfigPath), "openflare_access.log"), runner.Config.AccessLogPath,
[]byte("{\"ts\":\""+time.Now().UTC().Format(time.RFC3339)+"\",\"host\":\"edge.example.com\",\"path\":\"/\",\"remote_addr\":\"10.0.0.8\",\"status\":200}\n"), []byte("{\"ts\":\""+time.Now().UTC().Format(time.RFC3339)+"\",\"host\":\"edge.example.com\",\"path\":\"/\",\"remote_addr\":\"10.0.0.8\",\"status\":200}\n"),
0o644, 0o644,
); err != nil { ); err != nil {
+117 -42
View File
@@ -3,24 +3,26 @@ package config
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net" "net"
"openflare/utils/geoip/iputil" "openflare/utils/geoip/iputil"
"os" "os"
pathpkg "path" pathpkg "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
) )
const ( const (
defaultDockerMainConfigRelativePath = "etc/nginx/nginx.conf" defaultMainConfigRelativePath = "etc/nginx/nginx.conf"
defaultDockerRouteConfigRelativePath = "etc/nginx/conf.d/openflare_routes.conf" defaultRouteConfigRelativePath = "etc/nginx/conf.d/openflare_routes.conf"
defaultCertDirRelativePath = "etc/nginx/certs" defaultCertDirRelativePath = "etc/nginx/certs"
defaultLuaDirRelativePath = "etc/nginx/lua" defaultLuaDirRelativePath = "etc/nginx/lua"
defaultDockerStateRelativePath = "var/lib/openflare/agent-state.json" defaultRuntimeConfigDirRelativePath = "etc/openflare"
defaultAccessLogRelativePath = "var/log/openflare/access.log"
defaultStateRelativePath = "var/lib/openflare/agent-state.json"
defaultObservabilityBufferRelativePath = "var/lib/openflare/observability-buffer.json" defaultObservabilityBufferRelativePath = "var/lib/openflare/observability-buffer.json"
defaultDockerOpenRestyCertDir = "/etc/nginx/openflare-certs"
defaultDockerOpenRestyLuaDir = "/etc/nginx/openflare-lua"
defaultOpenRestyObservabilityPort = 18081 defaultOpenRestyObservabilityPort = 18081
defaultObservabilityReplayMinutes = 15 defaultObservabilityReplayMinutes = 15
) )
@@ -35,16 +37,18 @@ type Config struct {
NginxVersion string `json:"-"` NginxVersion string `json:"-"`
OpenrestyPath string `json:"openresty_path"` OpenrestyPath string `json:"openresty_path"`
OpenrestyResolvers []string `json:"openresty_resolvers,omitempty"` OpenrestyResolvers []string `json:"openresty_resolvers,omitempty"`
OpenrestyContainerName string `json:"openresty_container_name"` OpenrestyContainerName string `json:"openresty_container_name,omitempty"`
OpenrestyDockerImage string `json:"openresty_docker_image"` OpenrestyDockerImage string `json:"openresty_docker_image,omitempty"`
DockerBinary string `json:"docker_binary"` DockerBinary string `json:"docker_binary,omitempty"`
DataDir string `json:"data_dir"` DataDir string `json:"data_dir"`
MainConfigPath string `json:"main_config_path"` MainConfigPath string `json:"main_config_path"`
RouteConfigPath string `json:"route_config_path"` RouteConfigPath string `json:"route_config_path"`
AccessLogPath string `json:"access_log_path"`
CertDir string `json:"cert_dir"` CertDir string `json:"cert_dir"`
OpenrestyCertDir string `json:"openresty_cert_dir"` OpenrestyCertDir string `json:"openresty_cert_dir"`
LuaDir string `json:"lua_dir"` LuaDir string `json:"lua_dir"`
OpenrestyLuaDir string `json:"openresty_lua_dir"` OpenrestyLuaDir string `json:"openresty_lua_dir"`
RuntimeConfigDir string `json:"runtime_config_dir"`
OpenrestyObservabilityPort int `json:"openresty_observability_port"` OpenrestyObservabilityPort int `json:"openresty_observability_port"`
ObservabilityBufferPath string `json:"observability_buffer_path"` ObservabilityBufferPath string `json:"observability_buffer_path"`
ObservabilityReplayMinutes int `json:"observability_replay_minutes"` ObservabilityReplayMinutes int `json:"observability_replay_minutes"`
@@ -68,10 +72,12 @@ type configFile struct {
DataDir string `json:"data_dir"` DataDir string `json:"data_dir"`
MainConfigPath string `json:"main_config_path"` MainConfigPath string `json:"main_config_path"`
RouteConfigPath string `json:"route_config_path"` RouteConfigPath string `json:"route_config_path"`
AccessLogPath string `json:"access_log_path"`
CertDir string `json:"cert_dir"` CertDir string `json:"cert_dir"`
OpenrestyCertDir string `json:"openresty_cert_dir"` OpenrestyCertDir string `json:"openresty_cert_dir"`
LuaDir string `json:"lua_dir"` LuaDir string `json:"lua_dir"`
OpenrestyLuaDir string `json:"openresty_lua_dir"` OpenrestyLuaDir string `json:"openresty_lua_dir"`
RuntimeConfigDir string `json:"runtime_config_dir"`
OpenrestyObservabilityPort int `json:"openresty_observability_port"` OpenrestyObservabilityPort int `json:"openresty_observability_port"`
ObservabilityBufferPath string `json:"observability_buffer_path"` ObservabilityBufferPath string `json:"observability_buffer_path"`
ObservabilityReplayMinutes int `json:"observability_replay_minutes"` ObservabilityReplayMinutes int `json:"observability_replay_minutes"`
@@ -82,11 +88,16 @@ type configFile struct {
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil && !os.IsNotExist(err) {
return nil, err return nil, err
} }
file := &configFile{} file := &configFile{}
if err = json.Unmarshal(data, file); err != nil { if err == nil {
if err = json.Unmarshal(data, file); err != nil {
return nil, err
}
}
if err != nil && !hasEnvConfig() {
return nil, err return nil, err
} }
cfg := &Config{ cfg := &Config{
@@ -103,10 +114,12 @@ func Load(path string) (*Config, error) {
DataDir: file.DataDir, DataDir: file.DataDir,
MainConfigPath: file.MainConfigPath, MainConfigPath: file.MainConfigPath,
RouteConfigPath: file.RouteConfigPath, RouteConfigPath: file.RouteConfigPath,
AccessLogPath: file.AccessLogPath,
CertDir: file.CertDir, CertDir: file.CertDir,
OpenrestyCertDir: file.OpenrestyCertDir, OpenrestyCertDir: file.OpenrestyCertDir,
LuaDir: file.LuaDir, LuaDir: file.LuaDir,
OpenrestyLuaDir: file.OpenrestyLuaDir, OpenrestyLuaDir: file.OpenrestyLuaDir,
RuntimeConfigDir: file.RuntimeConfigDir,
OpenrestyObservabilityPort: file.OpenrestyObservabilityPort, OpenrestyObservabilityPort: file.OpenrestyObservabilityPort,
ObservabilityBufferPath: file.ObservabilityBufferPath, ObservabilityBufferPath: file.ObservabilityBufferPath,
ObservabilityReplayMinutes: file.ObservabilityReplayMinutes, ObservabilityReplayMinutes: file.ObservabilityReplayMinutes,
@@ -115,6 +128,7 @@ func Load(path string) (*Config, error) {
RequestTimeout: file.RequestTimeout, RequestTimeout: file.RequestTimeout,
} }
cfg.configPath = path cfg.configPath = path
applyEnvOverrides(cfg)
applyDefaults(cfg, filepath.Dir(path)) applyDefaults(cfg, filepath.Dir(path))
if err = validate(cfg); err != nil { if err = validate(cfg); err != nil {
return nil, err return nil, err
@@ -126,14 +140,8 @@ func applyDefaults(cfg *Config, baseDir string) {
baseDir = filepath.Clean(baseDir) baseDir = filepath.Clean(baseDir)
cfg.AgentVersion = AgentVersion cfg.AgentVersion = AgentVersion
cfg.OpenrestyResolvers = normalizeResolverList(cfg.OpenrestyResolvers) cfg.OpenrestyResolvers = normalizeResolverList(cfg.OpenrestyResolvers)
if cfg.OpenrestyContainerName == "" { if cfg.OpenrestyPath == "" {
cfg.OpenrestyContainerName = "openflare-openresty" cfg.OpenrestyPath = "openresty"
}
if cfg.OpenrestyDockerImage == "" {
cfg.OpenrestyDockerImage = "openresty/openresty:alpine"
}
if cfg.DockerBinary == "" {
cfg.DockerBinary = "docker"
} }
if cfg.DataDir == "" { if cfg.DataDir == "" {
cfg.DataDir = filepath.Join(baseDir, "data") cfg.DataDir = filepath.Join(baseDir, "data")
@@ -144,40 +152,32 @@ func applyDefaults(cfg *Config, baseDir string) {
if cfg.NodeIP == "" { if cfg.NodeIP == "" {
cfg.NodeIP = detectNodeIP() cfg.NodeIP = detectNodeIP()
} }
if cfg.OpenrestyPath == "" { if cfg.MainConfigPath == "" {
cfg.MainConfigPath = joinManagedPath(cfg.DataDir, defaultDockerMainConfigRelativePath) cfg.MainConfigPath = joinManagedPath(cfg.DataDir, defaultMainConfigRelativePath)
cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultDockerRouteConfigRelativePath) }
cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath) if cfg.RouteConfigPath == "" {
} else { cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultRouteConfigRelativePath)
if cfg.MainConfigPath == "" { }
cfg.MainConfigPath = joinManagedPath(cfg.DataDir, defaultDockerMainConfigRelativePath) if cfg.AccessLogPath == "" {
} cfg.AccessLogPath = joinManagedPath(cfg.DataDir, defaultAccessLogRelativePath)
if cfg.RouteConfigPath == "" { }
cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultDockerRouteConfigRelativePath) if cfg.StatePath == "" {
} cfg.StatePath = joinManagedPath(cfg.DataDir, defaultStateRelativePath)
if cfg.StatePath == "" {
cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath)
}
} }
if cfg.CertDir == "" { if cfg.CertDir == "" {
cfg.CertDir = joinManagedPath(cfg.DataDir, defaultCertDirRelativePath) cfg.CertDir = joinManagedPath(cfg.DataDir, defaultCertDirRelativePath)
} }
if cfg.OpenrestyCertDir == "" { if cfg.OpenrestyCertDir == "" {
if cfg.OpenrestyPath != "" { cfg.OpenrestyCertDir = cfg.CertDir
cfg.OpenrestyCertDir = cfg.CertDir
} else {
cfg.OpenrestyCertDir = defaultDockerOpenRestyCertDir
}
} }
if cfg.LuaDir == "" { if cfg.LuaDir == "" {
cfg.LuaDir = joinManagedPath(cfg.DataDir, defaultLuaDirRelativePath) cfg.LuaDir = joinManagedPath(cfg.DataDir, defaultLuaDirRelativePath)
} }
if cfg.OpenrestyLuaDir == "" { if cfg.OpenrestyLuaDir == "" {
if cfg.OpenrestyPath != "" { cfg.OpenrestyLuaDir = cfg.LuaDir
cfg.OpenrestyLuaDir = cfg.LuaDir }
} else { if cfg.RuntimeConfigDir == "" {
cfg.OpenrestyLuaDir = defaultDockerOpenRestyLuaDir cfg.RuntimeConfigDir = joinManagedPath(cfg.DataDir, defaultRuntimeConfigDirRelativePath)
}
} }
if cfg.OpenrestyObservabilityPort <= 0 { if cfg.OpenrestyObservabilityPort <= 0 {
cfg.OpenrestyObservabilityPort = defaultOpenRestyObservabilityPort cfg.OpenrestyObservabilityPort = defaultOpenRestyObservabilityPort
@@ -210,6 +210,9 @@ func normalizeManagedPaths(cfg *Config) {
if usesSlashPath(cfg.RouteConfigPath) { if usesSlashPath(cfg.RouteConfigPath) {
cfg.RouteConfigPath = filepath.ToSlash(cfg.RouteConfigPath) cfg.RouteConfigPath = filepath.ToSlash(cfg.RouteConfigPath)
} }
if usesSlashPath(cfg.AccessLogPath) {
cfg.AccessLogPath = filepath.ToSlash(cfg.AccessLogPath)
}
if usesSlashPath(cfg.CertDir) { if usesSlashPath(cfg.CertDir) {
cfg.CertDir = filepath.ToSlash(cfg.CertDir) cfg.CertDir = filepath.ToSlash(cfg.CertDir)
} }
@@ -222,6 +225,9 @@ func normalizeManagedPaths(cfg *Config) {
if usesSlashPath(cfg.OpenrestyLuaDir) { if usesSlashPath(cfg.OpenrestyLuaDir) {
cfg.OpenrestyLuaDir = filepath.ToSlash(cfg.OpenrestyLuaDir) cfg.OpenrestyLuaDir = filepath.ToSlash(cfg.OpenrestyLuaDir)
} }
if usesSlashPath(cfg.RuntimeConfigDir) {
cfg.RuntimeConfigDir = filepath.ToSlash(cfg.RuntimeConfigDir)
}
if usesSlashPath(cfg.StatePath) { if usesSlashPath(cfg.StatePath) {
cfg.StatePath = filepath.ToSlash(cfg.StatePath) cfg.StatePath = filepath.ToSlash(cfg.StatePath)
} }
@@ -230,6 +236,75 @@ func normalizeManagedPaths(cfg *Config) {
} }
} }
func hasEnvConfig() bool {
for _, key := range []string{
"OPENFLARE_SERVER_URL",
"OPENFLARE_AGENT_TOKEN",
"OPENFLARE_DISCOVERY_TOKEN",
"OPENFLARE_NODE_NAME",
"OPENFLARE_NODE_IP",
"OPENFLARE_DATA_DIR",
"OPENFLARE_OPENRESTY_PATH",
"OPENFLARE_HEARTBEAT_INTERVAL",
"OPENFLARE_REQUEST_TIMEOUT",
"OPENFLARE_OPENRESTY_OBSERVABILITY_PORT",
} {
if strings.TrimSpace(os.Getenv(key)) != "" {
return true
}
}
return false
}
func applyEnvOverrides(cfg *Config) {
if cfg == nil {
return
}
overrideString := func(key string, target *string) {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
*target = value
}
}
overrideString("OPENFLARE_SERVER_URL", &cfg.ServerURL)
overrideString("OPENFLARE_AGENT_TOKEN", &cfg.AgentToken)
overrideString("OPENFLARE_DISCOVERY_TOKEN", &cfg.DiscoveryToken)
overrideString("OPENFLARE_NODE_NAME", &cfg.NodeName)
overrideString("OPENFLARE_NODE_IP", &cfg.NodeIP)
overrideString("OPENFLARE_DATA_DIR", &cfg.DataDir)
overrideString("OPENFLARE_OPENRESTY_PATH", &cfg.OpenrestyPath)
if value := strings.TrimSpace(os.Getenv("OPENFLARE_HEARTBEAT_INTERVAL")); value != "" {
if duration, err := parseDurationValue(value); err == nil {
cfg.HeartbeatInterval = duration
}
}
if value := strings.TrimSpace(os.Getenv("OPENFLARE_REQUEST_TIMEOUT")); value != "" {
if duration, err := parseDurationValue(value); err == nil {
cfg.RequestTimeout = duration
}
}
if value := strings.TrimSpace(os.Getenv("OPENFLARE_OPENRESTY_OBSERVABILITY_PORT")); value != "" {
var port int
if _, err := fmt.Sscanf(value, "%d", &port); err == nil {
cfg.OpenrestyObservabilityPort = port
}
}
}
func parseDurationValue(value string) (MillisecondDuration, error) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return 0, nil
}
if parsed, err := time.ParseDuration(trimmed); err == nil {
return MillisecondDuration(parsed), nil
}
ms, err := strconv.ParseInt(trimmed, 10, 64)
if err != nil {
return 0, err
}
return MillisecondDuration(time.Duration(ms) * time.Millisecond), nil
}
func usesSlashPath(path string) bool { func usesSlashPath(path string) bool {
return strings.HasPrefix(path, "/") return strings.HasPrefix(path, "/")
} }
+128 -14
View File
@@ -9,7 +9,7 @@ import (
"time" "time"
) )
func TestLoadDockerModeUsesManagedPaths(t *testing.T) { func TestLoadDefaultsToManagedBinaryPaths(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
configPath := filepath.Join(dir, "agent.json") configPath := filepath.Join(dir, "agent.json")
payload := map[string]any{ payload := map[string]any{
@@ -34,31 +34,34 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) {
if cfg.DataDir != filepath.Join(dir, "data") { if cfg.DataDir != filepath.Join(dir, "data") {
t.Fatalf("unexpected data dir: %s", cfg.DataDir) t.Fatalf("unexpected data dir: %s", cfg.DataDir)
} }
if cfg.MainConfigPath != filepath.Join(dir, "data", defaultDockerMainConfigRelativePath) { if cfg.OpenrestyPath != "openresty" {
t.Fatalf("unexpected openresty path: %s", cfg.OpenrestyPath)
}
if cfg.MainConfigPath != filepath.Join(dir, "data", defaultMainConfigRelativePath) {
t.Fatalf("unexpected main config path: %s", cfg.MainConfigPath) t.Fatalf("unexpected main config path: %s", cfg.MainConfigPath)
} }
if cfg.RouteConfigPath != filepath.Join(dir, "data", defaultDockerRouteConfigRelativePath) { if cfg.RouteConfigPath != filepath.Join(dir, "data", defaultRouteConfigRelativePath) {
t.Fatalf("unexpected route config path: %s", cfg.RouteConfigPath) t.Fatalf("unexpected route config path: %s", cfg.RouteConfigPath)
} }
if cfg.AccessLogPath != filepath.Join(dir, "data", defaultAccessLogRelativePath) {
t.Fatalf("unexpected access log path: %s", cfg.AccessLogPath)
}
if cfg.CertDir != filepath.Join(dir, "data", defaultCertDirRelativePath) { if cfg.CertDir != filepath.Join(dir, "data", defaultCertDirRelativePath) {
t.Fatalf("unexpected cert dir: %s", cfg.CertDir) t.Fatalf("unexpected cert dir: %s", cfg.CertDir)
} }
if cfg.LuaDir != filepath.Join(dir, "data", defaultLuaDirRelativePath) { if cfg.LuaDir != filepath.Join(dir, "data", defaultLuaDirRelativePath) {
t.Fatalf("unexpected lua dir: %s", cfg.LuaDir) t.Fatalf("unexpected lua dir: %s", cfg.LuaDir)
} }
if cfg.OpenrestyContainerName != "openflare-openresty" { if cfg.RuntimeConfigDir != filepath.Join(dir, "data", defaultRuntimeConfigDirRelativePath) {
t.Fatalf("unexpected openresty container name: %s", cfg.OpenrestyContainerName) t.Fatalf("unexpected runtime config dir: %s", cfg.RuntimeConfigDir)
} }
if cfg.OpenrestyDockerImage != "openresty/openresty:alpine" { if cfg.OpenrestyCertDir != cfg.CertDir {
t.Fatalf("unexpected openresty image: %s", cfg.OpenrestyDockerImage)
}
if cfg.OpenrestyCertDir != defaultDockerOpenRestyCertDir {
t.Fatalf("unexpected openresty cert dir: %s", cfg.OpenrestyCertDir) t.Fatalf("unexpected openresty cert dir: %s", cfg.OpenrestyCertDir)
} }
if cfg.OpenrestyLuaDir != defaultDockerOpenRestyLuaDir { if cfg.OpenrestyLuaDir != cfg.LuaDir {
t.Fatalf("unexpected openresty lua dir: %s", cfg.OpenrestyLuaDir) t.Fatalf("unexpected openresty lua dir: %s", cfg.OpenrestyLuaDir)
} }
if cfg.StatePath != filepath.Join(dir, "data", defaultDockerStateRelativePath) { if cfg.StatePath != filepath.Join(dir, "data", defaultStateRelativePath) {
t.Fatalf("unexpected state path: %s", cfg.StatePath) t.Fatalf("unexpected state path: %s", cfg.StatePath)
} }
if cfg.ObservabilityBufferPath != filepath.Join(dir, "data", defaultObservabilityBufferRelativePath) { if cfg.ObservabilityBufferPath != filepath.Join(dir, "data", defaultObservabilityBufferRelativePath) {
@@ -155,6 +158,41 @@ func TestLoadNormalizesExplicitResolvers(t *testing.T) {
} }
} }
func TestLoadKeepsDeprecatedDockerFieldsForCompatibility(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "agent.json")
payload := map[string]any{
"server_url": "http://127.0.0.1:3000",
"agent_token": "token",
"node_name": "edge-01",
"node_ip": "10.0.0.8",
"openresty_container_name": "openflare-openresty",
"openresty_docker_image": "openresty/openresty:alpine",
"docker_binary": "docker",
}
data, err := json.Marshal(payload)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err = os.WriteFile(configPath, data, 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
cfg, err := Load(configPath)
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if cfg.OpenrestyContainerName != "openflare-openresty" {
t.Fatalf("unexpected container name: %s", cfg.OpenrestyContainerName)
}
if cfg.OpenrestyDockerImage != "openresty/openresty:alpine" {
t.Fatalf("unexpected image: %s", cfg.OpenrestyDockerImage)
}
if cfg.DockerBinary != "docker" {
t.Fatalf("unexpected docker binary: %s", cfg.DockerBinary)
}
}
func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
configPath := filepath.Join(dir, "agent.json") configPath := filepath.Join(dir, "agent.json")
@@ -178,13 +216,16 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) {
t.Fatalf("Load failed: %v", err) t.Fatalf("Load failed: %v", err)
} }
if cfg.RouteConfigPath != "/srv/openflare/"+defaultDockerRouteConfigRelativePath { if cfg.RouteConfigPath != "/srv/openflare/"+defaultRouteConfigRelativePath {
t.Fatalf("unexpected route config path: %s", cfg.RouteConfigPath) t.Fatalf("unexpected route config path: %s", cfg.RouteConfigPath)
} }
if cfg.MainConfigPath != "/srv/openflare/"+defaultDockerMainConfigRelativePath { if cfg.MainConfigPath != "/srv/openflare/"+defaultMainConfigRelativePath {
t.Fatalf("unexpected main config path: %s", cfg.MainConfigPath) t.Fatalf("unexpected main config path: %s", cfg.MainConfigPath)
} }
if cfg.StatePath != "/srv/openflare/"+defaultDockerStateRelativePath { if cfg.AccessLogPath != "/srv/openflare/"+defaultAccessLogRelativePath {
t.Fatalf("unexpected access log path: %s", cfg.AccessLogPath)
}
if cfg.StatePath != "/srv/openflare/"+defaultStateRelativePath {
t.Fatalf("unexpected state path: %s", cfg.StatePath) t.Fatalf("unexpected state path: %s", cfg.StatePath)
} }
if cfg.ObservabilityBufferPath != "/srv/openflare/"+defaultObservabilityBufferRelativePath { if cfg.ObservabilityBufferPath != "/srv/openflare/"+defaultObservabilityBufferRelativePath {
@@ -196,6 +237,70 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) {
if cfg.LuaDir != "/srv/openflare/"+defaultLuaDirRelativePath { if cfg.LuaDir != "/srv/openflare/"+defaultLuaDirRelativePath {
t.Fatalf("unexpected lua dir: %s", cfg.LuaDir) t.Fatalf("unexpected lua dir: %s", cfg.LuaDir)
} }
if cfg.RuntimeConfigDir != "/srv/openflare/"+defaultRuntimeConfigDirRelativePath {
t.Fatalf("unexpected runtime config dir: %s", cfg.RuntimeConfigDir)
}
}
func TestLoadUsesEnvConfigWhenFileIsMissing(t *testing.T) {
dir := t.TempDir()
t.Setenv("OPENFLARE_SERVER_URL", "http://127.0.0.1:3000")
t.Setenv("OPENFLARE_AGENT_TOKEN", "token")
t.Setenv("OPENFLARE_NODE_NAME", "edge-env")
t.Setenv("OPENFLARE_NODE_IP", "10.0.0.9")
t.Setenv("OPENFLARE_DATA_DIR", "/srv/openflare-env")
t.Setenv("OPENFLARE_OPENRESTY_PATH", "/usr/bin/openresty")
t.Setenv("OPENFLARE_HEARTBEAT_INTERVAL", "45s")
t.Setenv("OPENFLARE_REQUEST_TIMEOUT", "2500")
t.Setenv("OPENFLARE_OPENRESTY_OBSERVABILITY_PORT", "19091")
cfg, err := Load(filepath.Join(dir, "missing-agent.json"))
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if cfg.ServerURL != "http://127.0.0.1:3000" || cfg.AgentToken != "token" {
t.Fatalf("unexpected env auth config: %#v", cfg)
}
if cfg.OpenrestyPath != "/usr/bin/openresty" {
t.Fatalf("unexpected openresty path: %s", cfg.OpenrestyPath)
}
if cfg.DataDir != "/srv/openflare-env" {
t.Fatalf("unexpected data dir: %s", cfg.DataDir)
}
if cfg.HeartbeatInterval.Duration() != 45*time.Second {
t.Fatalf("unexpected heartbeat interval: %s", cfg.HeartbeatInterval)
}
if cfg.RequestTimeout.Duration() != 2500*time.Millisecond {
t.Fatalf("unexpected request timeout: %s", cfg.RequestTimeout)
}
if cfg.OpenrestyObservabilityPort != 19091 {
t.Fatalf("unexpected observability port: %d", cfg.OpenrestyObservabilityPort)
}
}
func TestLoadEnvOverridesConfigFile(t *testing.T) {
dir := t.TempDir()
configPath := filepath.Join(dir, "agent.json")
if err := os.WriteFile(configPath, []byte(`{"server_url":"http://old:3000","agent_token":"old","node_name":"edge-01","node_ip":"10.0.0.8","openresty_path":"/old/openresty"}`), 0o644); err != nil {
t.Fatalf("failed to write config: %v", err)
}
t.Setenv("OPENFLARE_SERVER_URL", "http://new:3000")
t.Setenv("OPENFLARE_AGENT_TOKEN", "new-token")
t.Setenv("OPENFLARE_OPENRESTY_PATH", "/new/openresty")
cfg, err := Load(configPath)
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if cfg.ServerURL != "http://new:3000" {
t.Fatalf("expected server url from env, got %s", cfg.ServerURL)
}
if cfg.AgentToken != "new-token" {
t.Fatalf("expected token from env, got %s", cfg.AgentToken)
}
if cfg.OpenrestyPath != "/new/openresty" {
t.Fatalf("expected openresty path from env, got %s", cfg.OpenrestyPath)
}
} }
func TestLoadUsesMillisecondsForIntervals(t *testing.T) { func TestLoadUsesMillisecondsForIntervals(t *testing.T) {
@@ -283,6 +388,15 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) {
if _, ok := decoded["nginx_path"]; ok { if _, ok := decoded["nginx_path"]; ok {
t.Fatal("legacy nginx_path should not be persisted") t.Fatal("legacy nginx_path should not be persisted")
} }
if _, ok := decoded["openresty_container_name"]; ok {
t.Fatal("deprecated openresty_container_name should not be persisted by default")
}
if _, ok := decoded["openresty_docker_image"]; ok {
t.Fatal("deprecated openresty_docker_image should not be persisted by default")
}
if _, ok := decoded["docker_binary"]; ok {
t.Fatal("deprecated docker_binary should not be persisted by default")
}
} }
func TestInitialAuthToken(t *testing.T) { func TestInitialAuthToken(t *testing.T) {
+92 -369
View File
@@ -24,15 +24,11 @@ const CertDirPlaceholder = "__OPENFLARE_CERT_DIR__"
const RouteConfigPlaceholder = "__OPENFLARE_ROUTE_CONFIG__" const RouteConfigPlaceholder = "__OPENFLARE_ROUTE_CONFIG__"
const AccessLogPlaceholder = "__OPENFLARE_ACCESS_LOG__" const AccessLogPlaceholder = "__OPENFLARE_ACCESS_LOG__"
const LuaDirPlaceholder = "__OPENFLARE_LUA_DIR__" const LuaDirPlaceholder = "__OPENFLARE_LUA_DIR__"
const RuntimeConfigDirPlaceholder = "__OPENFLARE_RUNTIME_CONFIG_DIR__"
const ObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__" const ObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__"
const ObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__" const ObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__"
const ResolverDirectivePlaceholder = "__OPENFLARE_RESOLVER_DIRECTIVE__" const ResolverDirectivePlaceholder = "__OPENFLARE_RESOLVER_DIRECTIVE__"
const PowStaticDirPlaceholder = "__OPENFLARE_POW_STATIC_DIR__" const PowStaticDirPlaceholder = "__OPENFLARE_POW_STATIC_DIR__"
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 { type Executor interface {
Test(ctx context.Context) error Test(ctx context.Context) error
@@ -55,13 +51,14 @@ func (r *OSCommandRunner) Run(ctx context.Context, name string, args ...string)
} }
type PathExecutor struct { type PathExecutor struct {
Path string Path string
Runner CommandRunner ConfigPath string
Runner CommandRunner
} }
func (e *PathExecutor) Test(ctx context.Context) error { func (e *PathExecutor) Test(ctx context.Context) error {
slog.Debug("running openresty test with binary", "path", e.Path) slog.Debug("running openresty test with binary", "path", e.Path, "config", e.ConfigPath)
output, err := e.Runner.Run(ctx, e.Path, "-t") output, err := e.Runner.Run(ctx, e.Path, "-t", "-c", e.ConfigPath)
if err != nil { if err != nil {
return fmt.Errorf("openresty -t failed: %w: %s", err, string(output)) return fmt.Errorf("openresty -t failed: %w: %s", err, string(output))
} }
@@ -70,9 +67,17 @@ func (e *PathExecutor) Test(ctx context.Context) error {
} }
func (e *PathExecutor) Reload(ctx context.Context) error { func (e *PathExecutor) Reload(ctx context.Context) error {
slog.Debug("running openresty reload with binary", "path", e.Path) slog.Debug("running openresty reload with binary", "path", e.Path, "config", e.ConfigPath)
output, err := e.Runner.Run(ctx, e.Path, "-s", "reload") output, err := e.Runner.Run(ctx, e.Path, "-s", "reload", "-c", e.ConfigPath)
if err != nil { if err != nil {
if isOpenrestyNotRunningError(string(output)) {
slog.Warn("openresty reload reported runtime is not running, starting binary", "path", e.Path)
startOutput, startErr := e.Runner.Run(ctx, e.Path, "-c", e.ConfigPath)
if startErr != nil {
return fmt.Errorf("openresty reload failed: %w: %s; start failed: %v: %s", err, string(output), startErr, string(startOutput))
}
return nil
}
return fmt.Errorf("openresty reload failed: %w: %s", err, string(output)) return fmt.Errorf("openresty reload failed: %w: %s", err, string(output))
} }
slog.Debug("openresty reload succeeded with binary", "path", e.Path) slog.Debug("openresty reload succeeded with binary", "path", e.Path)
@@ -80,7 +85,10 @@ func (e *PathExecutor) Reload(ctx context.Context) error {
} }
func (e *PathExecutor) EnsureRuntime(ctx context.Context, recreate bool) error { func (e *PathExecutor) EnsureRuntime(ctx context.Context, recreate bool) error {
return nil if err := e.Test(ctx); err != nil {
return err
}
return e.Reload(ctx)
} }
func (e *PathExecutor) CheckHealth(ctx context.Context) error { func (e *PathExecutor) CheckHealth(ctx context.Context) error {
@@ -88,15 +96,15 @@ func (e *PathExecutor) CheckHealth(ctx context.Context) error {
} }
func (e *PathExecutor) Restart(ctx context.Context) error { func (e *PathExecutor) Restart(ctx context.Context) error {
slog.Info("restarting openresty with binary", "path", e.Path) slog.Info("restarting openresty with binary", "path", e.Path, "config", e.ConfigPath)
output, err := e.Runner.Run(ctx, e.Path, "-s", "quit") output, err := e.Runner.Run(ctx, e.Path, "-s", "quit", "-c", e.ConfigPath)
if err != nil { if err != nil {
text := string(output) text := string(output)
if !isIgnorableOpenrestyStopError(text) { if !isIgnorableOpenrestyStopError(text) {
return fmt.Errorf("openresty stop failed: %w: %s", err, text) return fmt.Errorf("openresty stop failed: %w: %s", err, text)
} }
} }
output, err = e.Runner.Run(ctx, e.Path) output, err = e.Runner.Run(ctx, e.Path, "-c", e.ConfigPath)
if err != nil { if err != nil {
return fmt.Errorf("openresty start failed: %w: %s", err, string(output)) return fmt.Errorf("openresty start failed: %w: %s", err, string(output))
} }
@@ -104,258 +112,15 @@ func (e *PathExecutor) Restart(ctx context.Context) error {
return nil 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)
if err := e.validateMountSources(); err != nil {
return err
}
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 {
if err := e.validateMountSources(); err != nil {
return err
}
output, err := e.Runner.Run(ctx, e.DockerBinary, "inspect", "-f", "{{.State.Running}}", e.ContainerName)
if err != nil || strings.TrimSpace(string(output)) != "true" {
return e.EnsureRuntime(ctx, false)
}
output, err = e.Runner.Run(ctx, e.DockerBinary, "exec", e.ContainerName, dockerRuntimeCommand, "-s", "reload")
if err != nil {
if e.shouldRecreateAfterReloadFailure(string(output)) {
slog.Warn("docker openresty reload failed due to missing mounted files, recreating container", "container", e.ContainerName)
if recreateErr := e.EnsureRuntime(ctx, true); recreateErr != nil {
return fmt.Errorf("docker exec %s reload failed: %w: %s; recreate failed: %v", dockerRuntimeCommand, err, string(output), recreateErr)
}
return nil
}
return fmt.Errorf("docker exec %s reload failed: %w: %s", dockerRuntimeCommand, err, string(output))
}
return nil
}
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 e.containerNotRunningError(ctx)
}
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)
if err := e.validateMountSources(); err != nil {
return err
}
runArgs := []string{
"run", "-d",
"--name", e.ContainerName,
"--restart", "always",
"-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))
}
if err := e.CheckHealth(ctx); err != nil {
return err
}
slog.Info("docker openresty container started", "container", e.ContainerName)
return nil
}
func (e *DockerExecutor) validateMountSources() error {
if err := ensureRegularFile(e.MainConfigPath, "openresty main config"); err != nil {
return err
}
if err := ensureDirectory(e.RouteConfigDir, "openresty route config dir"); err != nil {
return err
}
if err := ensureDirectory(e.CertDir, "openresty cert dir"); err != nil {
return err
}
if err := ensureDirectory(e.LuaDir, "openresty lua dir"); err != nil {
return err
}
return nil
}
func (e *DockerExecutor) shouldRecreateAfterReloadFailure(output string) bool {
text := strings.ToLower(strings.TrimSpace(output))
if text == "" {
return false
}
if !strings.Contains(text, "no such file") && !strings.Contains(text, "cannot load certificate") {
return false
}
paths := []string{
strings.ToLower(e.NginxCertDir),
strings.ToLower(e.NginxLuaDir),
strings.ToLower(DockerMainConfigPath),
strings.ToLower("/etc/nginx/conf.d"),
}
for _, path := range paths {
if strings.TrimSpace(path) != "" && strings.Contains(text, path) {
return true
}
}
return false
}
func (e *DockerExecutor) containerNotRunningError(ctx context.Context) error {
inspectSummary := ""
inspectOutput, inspectErr := e.Runner.Run(ctx, e.DockerBinary, "inspect", "-f", "status={{.State.Status}} exit_code={{.State.ExitCode}} error={{printf \"%q\" .State.Error}} oom_killed={{.State.OOMKilled}} finished_at={{.State.FinishedAt}}", e.ContainerName)
if inspectErr == nil {
inspectSummary = strings.TrimSpace(string(inspectOutput))
}
logTail := ""
logOutput, logErr := e.Runner.Run(ctx, e.DockerBinary, "logs", "--tail", "50", e.ContainerName)
if logErr == nil {
logTail = strings.TrimSpace(string(logOutput))
}
message := "docker openresty container is not running"
if inspectSummary != "" {
message += ": " + inspectSummary
}
if logTail != "" {
message += "; recent logs: " + compactDiagnosticText(logTail)
}
return errors.New(message)
}
func compactDiagnosticText(text string) string {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return ""
}
lines := strings.Split(trimmed, "\n")
if len(lines) > 8 {
lines = lines[len(lines)-8:]
}
joined := strings.Join(lines, " | ")
joined = strings.Join(strings.Fields(joined), " ")
if len(joined) > 800 {
return joined[len(joined)-800:]
}
return joined
}
func ensureRegularFile(path string, label string) error {
cleanPath := strings.TrimSpace(path)
if cleanPath == "" {
return fmt.Errorf("%s path is empty", label)
}
info, err := os.Stat(cleanPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("%s %q does not exist; run a config apply first so Docker does not create a directory mount source", label, cleanPath)
}
return fmt.Errorf("stat %s %q failed: %w", label, cleanPath, err)
}
if info.IsDir() {
return fmt.Errorf("%s %q is a directory; expected a file for Docker bind mount", label, cleanPath)
}
return nil
}
func ensureDirectory(path string, label string) error {
cleanPath := strings.TrimSpace(path)
if cleanPath == "" {
return fmt.Errorf("%s path is empty", label)
}
info, err := os.Stat(cleanPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("%s %q does not exist; expected a directory for Docker bind mount", label, cleanPath)
}
return fmt.Errorf("stat %s %q failed: %w", label, cleanPath, err)
}
if !info.IsDir() {
return fmt.Errorf("%s %q is not a directory; expected a directory for Docker bind mount", label, cleanPath)
}
return nil
}
type Manager struct { type Manager struct {
MainConfigPath string MainConfigPath string
RouteConfigPath string RouteConfigPath string
RuntimeRouteConfigPath string AccessLogPath string
CertDir string CertDir string
NginxCertDir string NginxCertDir string
LuaDir string LuaDir string
NginxLuaDir string NginxLuaDir string
RuntimeConfigDir string
OpenrestyObservabilityListen string OpenrestyObservabilityListen string
OpenrestyObservabilityPort int OpenrestyObservabilityPort int
OpenrestyResolverDirective string OpenrestyResolverDirective string
@@ -419,8 +184,8 @@ func (m *Manager) activateConfig(ctx context.Context) error {
if m.Executor == nil { if m.Executor == nil {
return errors.New("executor 未配置") return errors.New("executor 未配置")
} }
if _, ok := m.Executor.(*DockerExecutor); ok { if err := m.Executor.Test(ctx); err != nil {
return m.Executor.EnsureRuntime(ctx, true) return err
} }
return m.Executor.Reload(ctx) return m.Executor.Reload(ctx)
} }
@@ -455,14 +220,7 @@ func (m *Manager) EnsureLuaAssets() error {
if strings.TrimSpace(m.LuaDir) == "" { if strings.TrimSpace(m.LuaDir) == "" {
return nil return nil
} }
allSupportFiles := append(ManagedObservabilityLuaFiles(), ManagedPowLuaFiles()...) allSupportFiles := append(ManagedObservabilityLuaFiles(), m.managedPowLuaFiles()...)
existingPowConfig, err := m.readPowConfigFile()
if err != nil {
return err
}
if existingPowConfig != nil {
allSupportFiles = append(allSupportFiles, *existingPowConfig)
}
powStaticFiles, err := ManagedPowStaticFiles() powStaticFiles, err := ManagedPowStaticFiles()
if err != nil { if err != nil {
return fmt.Errorf("load pow static files: %w", err) return fmt.Errorf("load pow static files: %w", err)
@@ -569,9 +327,6 @@ func (m *Manager) CurrentChecksum() (string, error) {
type ExecutorOptions struct { type ExecutorOptions struct {
NginxPath string NginxPath string
DockerBinary string
ContainerName string
Image string
MainConfigPath string MainConfigPath string
RouteConfigPath string RouteConfigPath string
CertDir string CertDir string
@@ -583,48 +338,10 @@ type ExecutorOptions struct {
func NewExecutor(options ExecutorOptions) Executor { func NewExecutor(options ExecutorOptions) Executor {
runner := &OSCommandRunner{} runner := &OSCommandRunner{}
if options.NginxPath != "" { return &PathExecutor{
return &PathExecutor{ Path: strings.TrimSpace(options.NginxPath),
Path: options.NginxPath, ConfigPath: strings.TrimSpace(options.MainConfigPath),
Runner: runner, 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,
} }
} }
@@ -653,15 +370,7 @@ func detectVersion(ctx context.Context, options ExecutorOptions, runner CommandR
} }
return version, nil return version, nil
} }
output, err := runDockerVersionProbe(ctx, runner, options.DockerBinary, options.Image) return "", errors.New("openresty path is empty")
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 { func parseNginxVersion(output string) string {
@@ -682,31 +391,14 @@ func isIgnorableOpenrestyStopError(output string) bool {
return strings.Contains(text, "invalid pid") || strings.Contains(text, "no such process") return strings.Contains(text, "invalid pid") || strings.Contains(text, "no such process")
} }
func (e *DockerExecutor) runEphemeralRuntimeCommand(ctx context.Context, args ...string) ([]byte, error) { func isOpenrestyNotRunningError(output string) bool {
return e.runEphemeralRuntimeCommandWithBinary(ctx, dockerRuntimeCommand, args...) text := strings.ToLower(strings.TrimSpace(output))
} if text == "" {
return false
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 strings.Contains(text, "invalid pid") ||
return e.Runner.Run(ctx, e.DockerBinary, runtimeArgs...) strings.Contains(text, "no such process") ||
} strings.Contains(text, "open()") && strings.Contains(text, "nginx.pid") && strings.Contains(text, "failed")
func runDockerVersionProbe(ctx context.Context, runner CommandRunner, dockerBinary string, image string) ([]byte, error) {
return runner.Run(ctx, dockerBinary, "run", "--rm", image, dockerRuntimeCommand, "-v")
} }
type backupState struct { type backupState struct {
@@ -737,11 +429,21 @@ func (m *Manager) backup() (*backupState, error) {
if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil {
return nil, err return nil, err
} }
if m.AccessLogPath != "" {
if err := os.MkdirAll(filepath.Dir(m.AccessLogPath), 0o755); err != nil {
return nil, err
}
}
if m.CertDir != "" { if m.CertDir != "" {
if err := os.MkdirAll(m.CertDir, 0o755); err != nil { if err := os.MkdirAll(m.CertDir, 0o755); err != nil {
return nil, err return nil, err
} }
} }
if m.RuntimeConfigDir != "" {
if err := os.MkdirAll(m.RuntimeConfigDir, 0o755); err != nil {
return nil, err
}
}
state := &backupState{} state := &backupState{}
mainData, err := os.ReadFile(m.MainConfigPath) mainData, err := os.ReadFile(m.MainConfigPath)
if err == nil { if err == nil {
@@ -806,10 +508,10 @@ func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error {
} }
func (m *Manager) writePowConfig(supportFiles []protocol.SupportFile) error { func (m *Manager) writePowConfig(supportFiles []protocol.SupportFile) error {
if m.LuaDir == "" { if m.RuntimeConfigDir == "" {
return nil return nil
} }
configPath := filepath.Join(m.LuaDir, "pow_config.json") configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json")
for _, file := range supportFiles { for _, file := range supportFiles {
if file.Path == "pow_config.json" { if file.Path == "pow_config.json" {
if err := os.WriteFile(configPath, []byte(file.Content), 0o644); err != nil { if err := os.WriteFile(configPath, []byte(file.Content), 0o644); err != nil {
@@ -822,12 +524,21 @@ func (m *Manager) writePowConfig(supportFiles []protocol.SupportFile) error {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) { if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove pow_config.json: %w", err) return fmt.Errorf("remove pow_config.json: %w", err)
} }
if err := removeLegacyPowConfig(filepath.Join(m.LuaDir, "pow_config.json")); err != nil {
return err
}
if err := removeLegacyPowConfig(filepath.Join(m.CertDir, "pow_config.json")); err != nil {
return err
}
return nil return nil
} }
func (m *Manager) writeManagedCertFiles(certFiles []protocol.SupportFile) error { func (m *Manager) writeManagedCertFiles(certFiles []protocol.SupportFile) error {
files := make([]managedFile, 0, len(certFiles)) files := make([]managedFile, 0, len(certFiles))
for _, file := range certFiles { for _, file := range certFiles {
if file.Path == "pow_config.json" {
continue
}
targetPath, err := m.certFileTargetPath(file.Path) targetPath, err := m.certFileTargetPath(file.Path)
if err != nil { if err != nil {
return err return err
@@ -863,11 +574,14 @@ func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) {
if info.IsDir() { if info.IsDir() {
return nil return nil
} }
data, err := os.ReadFile(path) relativePath, err := filepath.Rel(m.CertDir, path)
if err != nil { if err != nil {
return err return err
} }
relativePath, err := filepath.Rel(m.CertDir, path) if filepath.ToSlash(relativePath) == "pow_config.json" {
return nil
}
data, err := os.ReadFile(path)
if err != nil { if err != nil {
return err return err
} }
@@ -887,10 +601,10 @@ func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) {
} }
func (m *Manager) readPowConfigFile() (*protocol.SupportFile, error) { func (m *Manager) readPowConfigFile() (*protocol.SupportFile, error) {
if m.LuaDir == "" { if m.RuntimeConfigDir == "" {
return nil, nil return nil, nil
} }
configPath := filepath.Join(m.LuaDir, "pow_config.json") configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json")
data, err := os.ReadFile(configPath) data, err := os.ReadFile(configPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -920,10 +634,10 @@ func (m *Manager) readManagedSupportFiles() ([]protocol.SupportFile, error) {
} }
func (m *Manager) restorePowConfig(state *backupState) error { func (m *Manager) restorePowConfig(state *backupState) error {
if state == nil || m.LuaDir == "" { if state == nil || m.RuntimeConfigDir == "" {
return nil return nil
} }
configPath := filepath.Join(m.LuaDir, "pow_config.json") configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json")
if state.PowConfig == nil { if state.PowConfig == nil {
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) { if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
return err return err
@@ -933,6 +647,16 @@ func (m *Manager) restorePowConfig(state *backupState) error {
return os.WriteFile(configPath, []byte(state.PowConfig.Content), 0o644) return os.WriteFile(configPath, []byte(state.PowConfig.Content), 0o644)
} }
func removeLegacyPowConfig(path string) error {
if strings.TrimSpace(path) == "" {
return nil
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove legacy pow_config.json %q: %w", path, err)
}
return nil
}
func (m *Manager) certFileTargetPath(relativePath string) (string, error) { func (m *Manager) certFileTargetPath(relativePath string) (string, error) {
if strings.TrimSpace(m.CertDir) == "" { if strings.TrimSpace(m.CertDir) == "" {
return "", errors.New("cert dir 不能为空") return "", errors.New("cert dir 不能为空")
@@ -1119,14 +843,20 @@ func (m *Manager) renderMainConfig(content string) string {
return rendered return rendered
} }
func (m *Manager) managedPowLuaFiles() []protocol.SupportFile {
files := ManagedPowLuaFiles()
runtimeConfigDir := filepath.ToSlash(strings.TrimSpace(m.RuntimeConfigDir))
for index := range files {
files[index].Content = strings.ReplaceAll(files[index].Content, RuntimeConfigDirPlaceholder, runtimeConfigDir)
}
return files
}
func ObservabilityListenAddress(openrestyPath string, port int) string { func ObservabilityListenAddress(openrestyPath string, port int) string {
if port <= 0 { if port <= 0 {
return "" return ""
} }
if strings.TrimSpace(openrestyPath) != "" { return fmt.Sprintf("127.0.0.1:%d", port)
return fmt.Sprintf("127.0.0.1:%d", port)
}
return fmt.Sprintf("%d", port)
} }
func ResolverDirective(openrestyPath string, explicitResolvers []string) string { func ResolverDirective(openrestyPath string, explicitResolvers []string) string {
@@ -1145,7 +875,7 @@ func resolverAddresses(openrestyPath string, explicitResolvers []string) []strin
if err != nil { if err != nil {
return nil return nil
} }
return parseResolverAddresses(string(data), strings.TrimSpace(openrestyPath) == "") return parseResolverAddresses(string(data), false)
} }
func parseResolverAddresses(content string, dockerMode bool) []string { func parseResolverAddresses(content string, dockerMode bool) []string {
@@ -1213,18 +943,11 @@ func RequiresRuntimeResolver(originURL string) bool {
} }
func (m *Manager) routeConfigIncludePath() string { func (m *Manager) routeConfigIncludePath() string {
if strings.TrimSpace(m.RuntimeRouteConfigPath) != "" {
return strings.TrimSpace(m.RuntimeRouteConfigPath)
}
return strings.TrimSpace(m.RouteConfigPath) return strings.TrimSpace(m.RouteConfigPath)
} }
func (m *Manager) accessLogRuntimePath() string { func (m *Manager) accessLogRuntimePath() string {
includePath := m.routeConfigIncludePath() return filepath.ToSlash(strings.TrimSpace(m.AccessLogPath))
if strings.TrimSpace(includePath) == "" {
return ""
}
return filepath.ToSlash(filepath.Join(filepath.Dir(includePath), "openflare_access.log"))
} }
func (m *Manager) luaRuntimePath() string { func (m *Manager) luaRuntimePath() string {
+98 -490
View File
@@ -89,8 +89,9 @@ func (e *scriptedExecutor) Restart(ctx context.Context) error {
func TestPathExecutorCommands(t *testing.T) { func TestPathExecutorCommands(t *testing.T) {
runner := &fakeRunner{} runner := &fakeRunner{}
executor := &PathExecutor{ executor := &PathExecutor{
Path: "/usr/local/openresty/nginx/sbin/openresty", Path: "/usr/local/openresty/nginx/sbin/openresty",
Runner: runner, ConfigPath: "/data/etc/nginx/nginx.conf",
Runner: runner,
} }
if err := executor.Test(context.Background()); err != nil { if err := executor.Test(context.Background()); err != nil {
@@ -101,8 +102,8 @@ func TestPathExecutorCommands(t *testing.T) {
} }
expected := []runCall{ expected := []runCall{
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-t"}}, {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-t", "-c", "/data/etc/nginx/nginx.conf"}},
{name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload"}}, {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload", "-c", "/data/etc/nginx/nginx.conf"}},
} }
if !reflect.DeepEqual(runner.calls, expected) { if !reflect.DeepEqual(runner.calls, expected) {
t.Fatalf("unexpected calls: %#v", runner.calls) t.Fatalf("unexpected calls: %#v", runner.calls)
@@ -110,13 +111,18 @@ func TestPathExecutorCommands(t *testing.T) {
} }
func TestPathExecutorEnsureRuntimeNoop(t *testing.T) { func TestPathExecutorEnsureRuntimeNoop(t *testing.T) {
runner := &fakeRunner{}
executor := &PathExecutor{ executor := &PathExecutor{
Path: "/usr/local/openresty/nginx/sbin/openresty", Path: "/usr/local/openresty/nginx/sbin/openresty",
Runner: &fakeRunner{}, ConfigPath: "/data/etc/nginx/nginx.conf",
Runner: runner,
} }
if err := executor.EnsureRuntime(context.Background(), true); err != nil { if err := executor.EnsureRuntime(context.Background(), true); err != nil {
t.Fatalf("EnsureRuntime failed: %v", err) t.Fatalf("EnsureRuntime failed: %v", err)
} }
if len(runner.calls) != 2 {
t.Fatalf("expected test and reload calls, got %d", len(runner.calls))
}
} }
func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) { func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) {
@@ -129,8 +135,9 @@ func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) {
}, },
} }
executor := &PathExecutor{ executor := &PathExecutor{
Path: "/usr/local/openresty/nginx/sbin/openresty", Path: "/usr/local/openresty/nginx/sbin/openresty",
Runner: runner, ConfigPath: "/data/etc/nginx/nginx.conf",
Runner: runner,
} }
if err := executor.Restart(context.Background()); err != nil { if err := executor.Restart(context.Background()); err != nil {
t.Fatalf("Restart failed: %v", err) t.Fatalf("Restart failed: %v", err)
@@ -140,423 +147,32 @@ func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) {
} }
} }
func TestDockerExecutorCheckHealthFailsWhenContainerStopped(t *testing.T) { func TestPathExecutorReloadStartsWhenRuntimeIsNotRunning(t *testing.T) {
runner := &fakeRunner{ runner := &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) { runFn: func(name string, args ...string) ([]byte, error) {
if len(args) >= 4 && args[0] == "inspect" && args[2] == "{{.State.Running}}" { if len(args) >= 2 && args[0] == "-s" && args[1] == "reload" {
return []byte("false"), nil return []byte("openresty: [error] invalid PID number \"\" in \"/usr/local/openresty/nginx/logs/nginx.pid\""), errors.New("exit status 1")
} }
if len(args) >= 4 && args[0] == "inspect" { return []byte(""), nil
return []byte("status=exited exit_code=1 error=\"\" oom_killed=false finished_at=2026-03-18T10:08:30Z"), nil
}
if len(args) >= 1 && args[0] == "logs" {
return []byte("nginx: [emerg] host not found in upstream \"c1\" in /etc/nginx/conf.d/openflare_routes.conf:30"), nil
}
return []byte("false"), nil
}, },
} }
executor := &DockerExecutor{ executor := &PathExecutor{
DockerBinary: "docker", Path: "/usr/local/openresty/nginx/sbin/openresty",
ContainerName: "openflare-openresty", ConfigPath: "/data/etc/nginx/nginx.conf",
Image: "openresty/openresty:alpine", Runner: runner,
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")
} else {
text := err.Error()
if !strings.Contains(text, "exit_code=1") {
t.Fatalf("expected exit code in health error, got %v", err)
}
if !strings.Contains(text, "host not found in upstream") {
t.Fatalf("expected recent docker logs in health error, got %v", err)
}
}
}
func prepareDockerMountSources(t *testing.T) (string, string, string, string) {
t.Helper()
tempDir := t.TempDir()
mainConfigPath := filepath.Join(tempDir, "nginx.conf")
routeConfigDir := filepath.Join(tempDir, "conf.d")
certDir := filepath.Join(tempDir, "certs")
luaDir := filepath.Join(tempDir, "lua")
if err := os.WriteFile(mainConfigPath, []byte("events {}\nhttp {}\n"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
for _, dir := range []string{routeConfigDir, certDir, luaDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
}
return mainConfigPath, routeConfigDir, certDir, luaDir
}
func TestDockerExecutorStartsContainerWhenMissing(t *testing.T) {
mainConfigPath, routeConfigDir, certDir, luaDir := prepareDockerMountSources(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: mainConfigPath,
RouteConfigDir: routeConfigDir,
CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: luaDir,
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) {
mainConfigPath, routeConfigDir, certDir, luaDir := prepareDockerMountSources(t)
inspectCalls := 0
runner := &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) {
if len(args) >= 2 && args[0] == "inspect" {
inspectCalls++
if inspectCalls < 3 {
return []byte("false"), nil
}
return []byte("true"), nil
}
return []byte("ok"), nil
},
}
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",
Runner: runner,
}
if err := executor.Reload(context.Background()); err != nil { if err := executor.Reload(context.Background()); err != nil {
t.Fatalf("Reload failed: %v", err) t.Fatalf("Reload failed: %v", err)
} }
if len(runner.calls) != 5 {
t.Fatalf("expected 5 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] != "inspect" {
t.Fatalf("expected docker inspect on second call, got %#v", runner.calls[1])
}
if runner.calls[2].args[0] != "rm" {
t.Fatalf("expected docker rm on third call, got %#v", runner.calls[2])
}
if runner.calls[3].args[0] != "run" {
t.Fatalf("expected docker run on fourth call, got %#v", runner.calls[3])
}
if runner.calls[4].args[0] != "inspect" {
t.Fatalf("expected docker inspect after run, got %#v", runner.calls[4])
}
}
func TestDockerExecutorReloadsRunningContainerInPlace(t *testing.T) {
mainConfigPath, routeConfigDir, certDir, luaDir := prepareDockerMountSources(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: mainConfigPath,
RouteConfigDir: routeConfigDir,
CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: luaDir,
NginxLuaDir: "/etc/nginx/openflare-lua",
Runner: runner,
}
if err := executor.Reload(context.Background()); err != nil {
t.Fatalf("Reload failed: %v", err)
}
expected := []runCall{ expected := []runCall{
{name: "docker", args: []string{"inspect", "-f", "{{.State.Running}}", "openflare-openresty"}}, {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload", "-c", "/data/etc/nginx/nginx.conf"}},
{name: "docker", args: []string{"exec", "openflare-openresty", "openresty", "-s", "reload"}}, {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-c", "/data/etc/nginx/nginx.conf"}},
} }
if !reflect.DeepEqual(runner.calls, expected) { if !reflect.DeepEqual(runner.calls, expected) {
t.Fatalf("unexpected calls: %#v", runner.calls) t.Fatalf("unexpected calls: %#v", runner.calls)
} }
} }
func TestDockerExecutorReloadRecreatesContainerWhenMountedCertMissing(t *testing.T) {
mainConfigPath, routeConfigDir, certDir, luaDir := prepareDockerMountSources(t)
runner := &fakeRunner{
runFn: func(name string, args ...string) ([]byte, error) {
if len(args) >= 1 && args[0] == "inspect" {
return []byte("true"), nil
}
if len(args) >= 2 && args[0] == "exec" {
return []byte(`nginx: [emerg] cannot load certificate "/etc/nginx/openflare-certs/1.crt": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory)`), errors.New("exit status 1")
}
return []byte("ok"), nil
},
}
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.Reload(context.Background()); err != nil {
t.Fatalf("Reload failed: %v", err)
}
if len(runner.calls) != 6 {
t.Fatalf("expected 6 calls, got %d", len(runner.calls))
}
if runner.calls[2].args[0] != "inspect" || runner.calls[3].args[0] != "rm" || runner.calls[4].args[0] != "run" || runner.calls[5].args[0] != "inspect" {
t.Fatalf("expected recreate after reload failure, got %#v", runner.calls)
}
}
func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) {
mainConfigPath, routeConfigDir, certDir, luaDir := prepareDockerMountSources(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: 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) != 2 {
t.Fatalf("expected docker run plus health check, got %d calls", len(runner.calls))
}
expectedArgs := []string{
"run", "-d",
"--name", "openflare-openresty",
"--restart", "always",
"-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)
}
if !reflect.DeepEqual(runner.calls[1].args, []string{"inspect", "-f", "{{.State.Running}}", "openflare-openresty"}) {
t.Fatalf("unexpected docker health check args: %#v", runner.calls[1].args)
}
}
func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) {
mainConfigPath, routeConfigDir, certDir, luaDir := prepareDockerMountSources(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: mainConfigPath,
RouteConfigDir: routeConfigDir,
CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: luaDir,
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) != 4 {
t.Fatalf("expected 4 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])
}
if runner.calls[3].args[0] != "inspect" {
t.Fatalf("expected docker inspect after run, got %#v", runner.calls[3])
}
}
func TestDockerExecutorRunContainerRejectsMissingMainConfigFile(t *testing.T) {
tempDir := t.TempDir()
routeConfigDir := filepath.Join(tempDir, "conf.d")
certDir := filepath.Join(tempDir, "certs")
luaDir := filepath.Join(tempDir, "lua")
for _, dir := range []string{routeConfigDir, certDir, luaDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
}
executor := &DockerExecutor{
DockerBinary: "docker",
ContainerName: "openflare-openresty",
Image: "openresty/openresty:alpine",
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
RouteConfigDir: routeConfigDir,
CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: luaDir,
NginxLuaDir: "/etc/nginx/openflare-lua",
Runner: &fakeRunner{},
}
err := executor.runContainer(context.Background())
if err == nil {
t.Fatal("expected missing main config file to be rejected")
}
if !strings.Contains(err.Error(), "run a config apply first") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestDockerExecutorRunContainerRejectsMainConfigDirectory(t *testing.T) {
tempDir := t.TempDir()
mainConfigPath := filepath.Join(tempDir, "nginx.conf")
routeConfigDir := filepath.Join(tempDir, "conf.d")
certDir := filepath.Join(tempDir, "certs")
luaDir := filepath.Join(tempDir, "lua")
for _, dir := range []string{mainConfigPath, routeConfigDir, certDir, luaDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
}
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",
Runner: &fakeRunner{},
}
err := executor.runContainer(context.Background())
if err == nil {
t.Fatal("expected main config directory to be rejected")
}
if !strings.Contains(err.Error(), "expected a file") {
t.Fatalf("unexpected error: %v", err)
}
}
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) { func TestDetectVersionFromBinary(t *testing.T) {
version, err := detectVersion(context.Background(), ExecutorOptions{ version, err := detectVersion(context.Background(), ExecutorOptions{
NginxPath: "/usr/local/openresty/nginx/sbin/openresty", NginxPath: "/usr/local/openresty/nginx/sbin/openresty",
@@ -578,9 +194,11 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) {
mainPath := filepath.Join(tempDir, "nginx.conf") mainPath := filepath.Join(tempDir, "nginx.conf")
routePath := filepath.Join(tempDir, "conf.d", "openflare_routes.conf") routePath := filepath.Join(tempDir, "conf.d", "openflare_routes.conf")
certDir := filepath.Join(tempDir, "certs") certDir := filepath.Join(tempDir, "certs")
accessLogPath := filepath.Join(tempDir, "var", "log", "openflare", "access.log")
manager := &Manager{ manager := &Manager{
MainConfigPath: mainPath, MainConfigPath: mainPath,
RouteConfigPath: routePath, RouteConfigPath: routePath,
AccessLogPath: accessLogPath,
CertDir: certDir, CertDir: certDir,
NginxCertDir: "/etc/nginx/openflare-certs", NginxCertDir: "/etc/nginx/openflare-certs",
LuaDir: filepath.Join(tempDir, "lua"), LuaDir: filepath.Join(tempDir, "lua"),
@@ -602,7 +220,7 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to read main config: %v", err) t.Fatalf("failed to read main config: %v", err)
} }
expectedMain := "include " + routePath + ";\naccess_log " + filepath.ToSlash(filepath.Join(filepath.Dir(routePath), "openflare_access.log")) + " openflare_json;\n" expectedMain := "include " + routePath + ";\naccess_log " + filepath.ToSlash(accessLogPath) + " openflare_json;\n"
if string(mainData) != expectedMain { if string(mainData) != expectedMain {
t.Fatalf("unexpected main config: %s", string(mainData)) t.Fatalf("unexpected main config: %s", string(mainData))
} }
@@ -629,73 +247,6 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) {
} }
} }
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 outcome := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\naccess_log __OPENFLARE_ACCESS_LOG__ openflare_json;\n", "server { listen 80; }\n", nil); outcome.Status != ApplyStatusSuccess {
t.Fatalf("Apply failed: %#v", outcome)
}
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) { func TestParseNginxVersionIgnoresDockerEntrypointPaths(t *testing.T) {
output := strings.Join([]string{ output := strings.Join([]string{
"/docker-entrypoint.sh: /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)", "/docker-entrypoint.sh: /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh: info: can not modify /etc/nginx/conf.d/default.conf (read-only file system?)",
@@ -904,8 +455,9 @@ func TestCertFileMode(t *testing.T) {
func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) { func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
manager := &Manager{ manager := &Manager{
LuaDir: filepath.Join(tempDir, "lua"), LuaDir: filepath.Join(tempDir, "lua"),
NginxLuaDir: "/etc/nginx/openflare-lua", NginxLuaDir: "/etc/nginx/openflare-lua",
RuntimeConfigDir: filepath.Join(tempDir, "runtime"),
} }
err := manager.EnsureLuaAssets() err := manager.EnsureLuaAssets()
@@ -923,20 +475,28 @@ func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) {
if _, err := os.Stat(filepath.Join(manager.LuaDir, "pow", "check.lua")); err != nil { if _, err := os.Stat(filepath.Join(manager.LuaDir, "pow", "check.lua")); err != nil {
t.Fatalf("failed to stat pow lua file: %v", err) t.Fatalf("failed to stat pow lua file: %v", err)
} }
data, err := os.ReadFile(filepath.Join(manager.LuaDir, "pow", "check.lua"))
if err != nil {
t.Fatalf("failed to read pow lua file: %v", err)
}
if !strings.Contains(string(data), filepath.ToSlash(manager.RuntimeConfigDir)+"/pow_config.json") {
t.Fatalf("expected pow lua to read runtime config dir, got %s", string(data))
}
} }
func TestEnsureLuaAssetsPreservesPowConfig(t *testing.T) { func TestEnsureLuaAssetsLeavesRuntimePowConfigOutsideLuaDir(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
luaDir := filepath.Join(tempDir, "lua") luaDir := filepath.Join(tempDir, "lua")
if err := os.MkdirAll(luaDir, 0o755); err != nil { runtimeConfigDir := filepath.Join(tempDir, "runtime")
if err := os.MkdirAll(runtimeConfigDir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err) t.Fatalf("MkdirAll failed: %v", err)
} }
powConfigPath := filepath.Join(luaDir, "pow_config.json") powConfigPath := filepath.Join(runtimeConfigDir, "pow_config.json")
want := `[{"domains":["pow.example.com"],"enabled":true}]` want := `[{"domains":["pow.example.com"],"enabled":true}]`
if err := os.WriteFile(powConfigPath, []byte(want), 0o644); err != nil { if err := os.WriteFile(powConfigPath, []byte(want), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err) t.Fatalf("WriteFile failed: %v", err)
} }
manager := &Manager{LuaDir: luaDir} manager := &Manager{LuaDir: luaDir, RuntimeConfigDir: runtimeConfigDir}
if err := manager.EnsureLuaAssets(); err != nil { if err := manager.EnsureLuaAssets(); err != nil {
t.Fatalf("EnsureLuaAssets failed: %v", err) t.Fatalf("EnsureLuaAssets failed: %v", err)
@@ -949,6 +509,52 @@ func TestEnsureLuaAssetsPreservesPowConfig(t *testing.T) {
if string(got) != want { if string(got) != want {
t.Fatalf("unexpected pow_config.json content: got %s want %s", string(got), want) t.Fatalf("unexpected pow_config.json content: got %s want %s", string(got), want)
} }
if _, err := os.Stat(filepath.Join(luaDir, "pow_config.json")); !os.IsNotExist(err) {
t.Fatalf("expected lua pow_config.json to stay absent, stat err = %v", err)
}
}
func TestManagerApplyWritesPowConfigToRuntimeDirAndCleansLegacyCopies(t *testing.T) {
tempDir := t.TempDir()
certDir := filepath.Join(tempDir, "certs")
luaDir := filepath.Join(tempDir, "lua")
runtimeConfigDir := filepath.Join(tempDir, "runtime")
for _, dir := range []string{certDir, luaDir, runtimeConfigDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll failed: %v", err)
}
}
for _, path := range []string{filepath.Join(certDir, "pow_config.json"), filepath.Join(luaDir, "pow_config.json")} {
if err := os.WriteFile(path, []byte("stale"), 0o644); err != nil {
t.Fatalf("WriteFile failed: %v", err)
}
}
manager := &Manager{
MainConfigPath: filepath.Join(tempDir, "nginx.conf"),
RouteConfigPath: filepath.Join(tempDir, "routes.conf"),
CertDir: certDir,
LuaDir: luaDir,
RuntimeConfigDir: runtimeConfigDir,
Executor: &fakeExecutor{},
}
outcome := manager.Apply(context.Background(), "main", "route", []protocol.SupportFile{
{Path: "pow_config.json", Content: "runtime"},
})
if outcome.Status != ApplyStatusSuccess {
t.Fatalf("Apply failed: %#v", outcome)
}
data, err := os.ReadFile(filepath.Join(runtimeConfigDir, "pow_config.json"))
if err != nil {
t.Fatalf("failed to read runtime pow config: %v", err)
}
if string(data) != "runtime" {
t.Fatalf("unexpected runtime pow config: %s", string(data))
}
for _, path := range []string{filepath.Join(certDir, "pow_config.json"), filepath.Join(luaDir, "pow_config.json")} {
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("expected legacy pow config to be removed from %s, stat err = %v", path, err)
}
}
} }
func TestManagerCurrentChecksumIncludesPowConfig(t *testing.T) { func TestManagerCurrentChecksumIncludesPowConfig(t *testing.T) {
@@ -956,12 +562,14 @@ func TestManagerCurrentChecksumIncludesPowConfig(t *testing.T) {
mainPath := filepath.Join(tempDir, "nginx.conf") mainPath := filepath.Join(tempDir, "nginx.conf")
routePath := filepath.Join(tempDir, "routes.conf") routePath := filepath.Join(tempDir, "routes.conf")
luaDir := filepath.Join(tempDir, "lua") luaDir := filepath.Join(tempDir, "lua")
runtimeConfigDir := filepath.Join(tempDir, "runtime")
manager := &Manager{ manager := &Manager{
MainConfigPath: mainPath, MainConfigPath: mainPath,
RouteConfigPath: routePath, RouteConfigPath: routePath,
LuaDir: luaDir, LuaDir: luaDir,
NginxLuaDir: "/etc/nginx/openflare-lua", NginxLuaDir: "/etc/nginx/openflare-lua",
Executor: &fakeExecutor{}, RuntimeConfigDir: runtimeConfigDir,
Executor: &fakeExecutor{},
} }
outcome := manager.Apply( outcome := manager.Apply(
@@ -1189,8 +797,8 @@ func TestManagerApplyRejectsCertFilePathTraversal(t *testing.T) {
} }
func TestObservabilityListenAddress(t *testing.T) { func TestObservabilityListenAddress(t *testing.T) {
if got := ObservabilityListenAddress("", 18081); got != "18081" { if got := ObservabilityListenAddress("", 18081); got != "127.0.0.1:18081" {
t.Fatalf("unexpected docker observability listen address: %s", got) t.Fatalf("unexpected default observability listen address: %s", got)
} }
if got := ObservabilityListenAddress("/usr/local/openresty/nginx/sbin/openresty", 18081); got != "127.0.0.1:18081" { 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) t.Fatalf("unexpected path observability listen address: %s", got)
+1 -1
View File
@@ -36,7 +36,7 @@ end
-- Lazy-load pow_config from file; reload when content changes -- Lazy-load pow_config from file; reload when content changes
local function load_pow_config() local function load_pow_config()
local config_paths = { local config_paths = {
ngx.config.prefix() .. "openflare-lua/pow_config.json", "__OPENFLARE_RUNTIME_CONFIG_DIR__/pow_config.json",
"/etc/nginx/openflare-lua/pow_config.json", "/etc/nginx/openflare-lua/pow_config.json",
"/usr/local/openresty/nginx/conf/pow_config.json" "/usr/local/openresty/nginx/conf/pow_config.json"
} }
@@ -9,7 +9,6 @@ import (
"openflare-agent/internal/protocol" "openflare-agent/internal/protocol"
"openflare-agent/internal/state" "openflare-agent/internal/state"
"os" "os"
"path/filepath"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
@@ -128,10 +127,10 @@ func readAccessLogDelta(cfg *config.Config, stateStore *state.Store) *trafficAgg
} }
func managedAccessLogPath(cfg *config.Config) string { func managedAccessLogPath(cfg *config.Config) string {
if cfg == nil || strings.TrimSpace(cfg.RouteConfigPath) == "" { if cfg == nil || strings.TrimSpace(cfg.AccessLogPath) == "" {
return "" return ""
} }
return filepath.Join(filepath.Dir(cfg.RouteConfigPath), "openflare_access.log") return cfg.AccessLogPath
} }
func newTrafficAggregate() *trafficAggregate { func newTrafficAggregate() *trafficAggregate {
@@ -28,7 +28,7 @@ func TestBuildTrafficReportAggregatesManagedAccessLog(t *testing.T) {
} }
stateStore := state.NewStore(filepath.Join(tempDir, "state.json")) stateStore := state.NewStore(filepath.Join(tempDir, "state.json"))
report := BuildTrafficReport(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) report := BuildTrafficReport(&config.Config{AccessLogPath: logPath}, stateStore, nil)
if report == nil { if report == nil {
t.Fatal("expected traffic report") t.Fatal("expected traffic report")
} }
@@ -50,7 +50,7 @@ func TestBuildTrafficReportAggregatesManagedAccessLog(t *testing.T) {
t.Fatalf("unexpected access log offset: %d", snapshot.AccessLogOffset) t.Fatalf("unexpected access log offset: %d", snapshot.AccessLogOffset)
} }
secondReport := BuildTrafficReport(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) secondReport := BuildTrafficReport(&config.Config{AccessLogPath: logPath}, stateStore, nil)
if secondReport != nil { if secondReport != nil {
t.Fatalf("expected no report without appended lines, got %+v", secondReport) t.Fatalf("expected no report without appended lines, got %+v", secondReport)
} }
@@ -72,7 +72,7 @@ func TestBuildTrafficReportResetsOffsetAfterTruncate(t *testing.T) {
t.Fatalf("Save failed: %v", err) t.Fatalf("Save failed: %v", err)
} }
report := BuildTrafficReport(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) report := BuildTrafficReport(&config.Config{AccessLogPath: logPath}, stateStore, nil)
if report == nil || report.RequestCount != 1 { if report == nil || report.RequestCount != 1 {
t.Fatalf("expected one request after truncate reset, got %+v", report) t.Fatalf("expected one request after truncate reset, got %+v", report)
} }
@@ -94,7 +94,7 @@ func TestBuildTrafficObservabilityReturnsAccessLogs(t *testing.T) {
} }
stateStore := state.NewStore(filepath.Join(tempDir, "state.json")) stateStore := state.NewStore(filepath.Join(tempDir, "state.json"))
report, accessLogs, fallbackMetrics := BuildTrafficObservability(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) report, accessLogs, fallbackMetrics := BuildTrafficObservability(&config.Config{AccessLogPath: logPath}, stateStore, nil)
if report == nil || report.RequestCount != 2 { if report == nil || report.RequestCount != 2 {
t.Fatalf("expected traffic report, got %+v", report) t.Fatalf("expected traffic report, got %+v", report)
} }
@@ -125,7 +125,7 @@ func TestBuildTrafficObservabilityTruncatesLongAccessLogPath(t *testing.T) {
} }
stateStore := state.NewStore(filepath.Join(tempDir, "state.json")) stateStore := state.NewStore(filepath.Join(tempDir, "state.json"))
_, accessLogs, _ := BuildTrafficObservability(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) _, accessLogs, _ := BuildTrafficObservability(&config.Config{AccessLogPath: logPath}, stateStore, nil)
if len(accessLogs) != 1 { if len(accessLogs) != 1 {
t.Fatalf("expected one access log, got %+v", accessLogs) t.Fatalf("expected one access log, got %+v", accessLogs)
} }
@@ -151,7 +151,7 @@ func TestBuildTrafficReportParsesCombinedAccessLog(t *testing.T) {
} }
stateStore := state.NewStore(filepath.Join(tempDir, "state.json")) stateStore := state.NewStore(filepath.Join(tempDir, "state.json"))
report := BuildTrafficReport(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) report := BuildTrafficReport(&config.Config{AccessLogPath: logPath}, stateStore, nil)
if report == nil { if report == nil {
t.Fatal("expected traffic report from combined access log") t.Fatal("expected traffic report from combined access log")
} }
+20 -2
View File
@@ -14,6 +14,7 @@ DISCOVERY_TOKEN=""
AGENT_TOKEN="" AGENT_TOKEN=""
CREATE_SERVICE="true" CREATE_SERVICE="true"
SERVICE_NAME="openflare-agent" SERVICE_NAME="openflare-agent"
OPENRESTY_PATH=""
usage() { usage() {
cat <<EOF cat <<EOF
@@ -27,6 +28,7 @@ Options:
--discovery-token TOKEN Discovery token for auto-registration --discovery-token TOKEN Discovery token for auto-registration
--agent-token TOKEN Node-specific agent token --agent-token TOKEN Node-specific agent token
--install-dir DIR Installation directory (default: /opt/openflare-agent) --install-dir DIR Installation directory (default: /opt/openflare-agent)
--openresty-path PATH OpenResty binary path (default: auto-detect from PATH)
--repo REPO GitHub repository (default: Rain-kl/OpenFlare) --repo REPO GitHub repository (default: Rain-kl/OpenFlare)
--no-service Do not create systemd service --no-service Do not create systemd service
-h, --help Show this help message -h, --help Show this help message
@@ -51,6 +53,7 @@ while [[ $# -gt 0 ]]; do
--discovery-token) DISCOVERY_TOKEN="$2"; shift 2 ;; --discovery-token) DISCOVERY_TOKEN="$2"; shift 2 ;;
--agent-token) AGENT_TOKEN="$2"; shift 2 ;; --agent-token) AGENT_TOKEN="$2"; shift 2 ;;
--install-dir) INSTALL_DIR="$2"; shift 2 ;; --install-dir) INSTALL_DIR="$2"; shift 2 ;;
--openresty-path) OPENRESTY_PATH="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;; --repo) REPO="$2"; shift 2 ;;
--no-service) CREATE_SERVICE="false"; shift ;; --no-service) CREATE_SERVICE="false"; shift ;;
-h|--help) usage ;; -h|--help) usage ;;
@@ -82,6 +85,20 @@ if [[ "$OS" != "linux" && "$OS" != "darwin" ]]; then
exit 1 exit 1
fi fi
if [[ -z "$OPENRESTY_PATH" ]]; then
if command -v openresty >/dev/null 2>&1; then
OPENRESTY_PATH="$(command -v openresty)"
else
echo "Error: openresty was not found in PATH. Install OpenResty first or pass --openresty-path."
exit 1
fi
fi
if [[ ! -x "$OPENRESTY_PATH" ]]; then
echo "Error: OpenResty binary is not executable: ${OPENRESTY_PATH}"
exit 1
fi
ASSET_NAME="openflare-agent-${OS}-${ARCH}" ASSET_NAME="openflare-agent-${OS}-${ARCH}"
echo "Detected platform: ${OS}/${ARCH}" echo "Detected platform: ${OS}/${ARCH}"
@@ -140,9 +157,9 @@ if [[ -n "$AGENT_TOKEN" ]]; then
{ {
"server_url": "${SERVER_URL}", "server_url": "${SERVER_URL}",
"agent_token": "${AGENT_TOKEN}", "agent_token": "${AGENT_TOKEN}",
"openresty_path": "${OPENRESTY_PATH}",
"data_dir": "${INSTALL_DIR}/data", "data_dir": "${INSTALL_DIR}/data",
"heartbeat_interval": 30000, "heartbeat_interval": 30000,
"sync_interval": 30000,
"request_timeout": 10000 "request_timeout": 10000
} }
CFGEOF CFGEOF
@@ -151,9 +168,9 @@ else
{ {
"server_url": "${SERVER_URL}", "server_url": "${SERVER_URL}",
"discovery_token": "${DISCOVERY_TOKEN}", "discovery_token": "${DISCOVERY_TOKEN}",
"openresty_path": "${OPENRESTY_PATH}",
"data_dir": "${INSTALL_DIR}/data", "data_dir": "${INSTALL_DIR}/data",
"heartbeat_interval": 30000, "heartbeat_interval": 30000,
"sync_interval": 30000,
"request_timeout": 10000 "request_timeout": 10000
} }
CFGEOF CFGEOF
@@ -197,3 +214,4 @@ echo "OpenFlare Agent installed successfully!"
echo " Binary: ${INSTALL_DIR}/openflare-agent" echo " Binary: ${INSTALL_DIR}/openflare-agent"
echo " Config: ${CONFIG_FILE}" echo " Config: ${CONFIG_FILE}"
echo " Data: ${INSTALL_DIR}/data" echo " Data: ${INSTALL_DIR}/data"
echo " OpenResty: ${OPENRESTY_PATH}"
+2 -92
View File
@@ -19,9 +19,7 @@ Options:
Behavior: Behavior:
1. Stop the agent service/process and remove the entire installation directory 1. Stop the agent service/process and remove the entire installation directory
2. Remove the systemd service definition when present 2. Remove the systemd service definition when present
3. Check the saved agent config to identify the OpenResty mode 3. Leave the local OpenResty installation untouched
4. If Docker mode was used, remove the OpenResty container and try to remove its image
5. If local openresty_path mode was used, do not modify the local OpenResty install
Examples: Examples:
uninstall-agent.sh uninstall-agent.sh
@@ -44,52 +42,9 @@ if [[ -z "$INSTALL_DIR" || "$INSTALL_DIR" == "/" || "$INSTALL_DIR" == "." ]]; th
exit 1 exit 1
fi fi
json_get_string() {
local file="$1"
local key="$2"
local match
match=$(grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$file" 2>/dev/null | head -n 1 || true)
if [[ -z "$match" ]]; then
return 0
fi
printf '%s\n' "$match" | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/'
}
AGENT_BINARY="${INSTALL_DIR}/openflare-agent" AGENT_BINARY="${INSTALL_DIR}/openflare-agent"
CONFIG_FILE="${INSTALL_DIR}/agent.json"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
OPENRESTY_PATH=""
OPENRESTY_CONTAINER_NAME="openflare-openresty"
OPENRESTY_DOCKER_IMAGE="openresty/openresty:alpine"
DOCKER_BINARY="docker"
OPENRESTY_MODE="unknown"
if [[ -f "$CONFIG_FILE" ]]; then
OPENRESTY_PATH="$(json_get_string "$CONFIG_FILE" "openresty_path")"
OPENRESTY_CONTAINER_NAME="$(json_get_string "$CONFIG_FILE" "openresty_container_name")"
OPENRESTY_DOCKER_IMAGE="$(json_get_string "$CONFIG_FILE" "openresty_docker_image")"
DOCKER_BINARY="$(json_get_string "$CONFIG_FILE" "docker_binary")"
if [[ -z "$OPENRESTY_CONTAINER_NAME" ]]; then
OPENRESTY_CONTAINER_NAME="openflare-openresty"
fi
if [[ -z "$OPENRESTY_DOCKER_IMAGE" ]]; then
OPENRESTY_DOCKER_IMAGE="openresty/openresty:alpine"
fi
if [[ -z "$DOCKER_BINARY" ]]; then
DOCKER_BINARY="docker"
fi
if [[ -n "$OPENRESTY_PATH" ]]; then
OPENRESTY_MODE="local"
else
OPENRESTY_MODE="docker"
fi
fi
SYSTEMCTL_AVAILABLE="false" SYSTEMCTL_AVAILABLE="false"
if command -v systemctl >/dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
SYSTEMCTL_AVAILABLE="true" SYSTEMCTL_AVAILABLE="true"
@@ -143,50 +98,5 @@ fi
echo "Agent uninstall complete." echo "Agent uninstall complete."
echo "" echo ""
echo "Checking OpenResty installation mode..." echo "Local OpenResty was not modified. Remove it manually if you no longer need it."
if [[ "$OPENRESTY_MODE" == "docker" ]]; then
echo "Detected Docker OpenResty mode."
if ! command -v "$DOCKER_BINARY" >/dev/null 2>&1; then
echo "Docker binary '${DOCKER_BINARY}' was not found."
echo "Please remove container '${OPENRESTY_CONTAINER_NAME}' and image '${OPENRESTY_DOCKER_IMAGE}' manually."
exit 0
fi
if "$DOCKER_BINARY" inspect "$OPENRESTY_CONTAINER_NAME" >/dev/null 2>&1; then
if [[ -z "$OPENRESTY_DOCKER_IMAGE" ]]; then
OPENRESTY_DOCKER_IMAGE="$("$DOCKER_BINARY" inspect -f '{{.Config.Image}}' "$OPENRESTY_CONTAINER_NAME" 2>/dev/null || true)"
fi
echo "Removing Docker container: ${OPENRESTY_CONTAINER_NAME}"
"$DOCKER_BINARY" rm -f "$OPENRESTY_CONTAINER_NAME"
else
echo "Docker container not found, skipping: ${OPENRESTY_CONTAINER_NAME}"
fi
if [[ -n "$OPENRESTY_DOCKER_IMAGE" ]] && "$DOCKER_BINARY" image inspect "$OPENRESTY_DOCKER_IMAGE" >/dev/null 2>&1; then
other_container_ids="$("$DOCKER_BINARY" ps -a --filter "ancestor=${OPENRESTY_DOCKER_IMAGE}" --format '{{.ID}}' 2>/dev/null || true)"
if [[ -z "$other_container_ids" ]]; then
echo "Removing Docker image: ${OPENRESTY_DOCKER_IMAGE}"
if ! "$DOCKER_BINARY" image rm "$OPENRESTY_DOCKER_IMAGE"; then
echo "Image removal skipped because Docker reported it is still in use."
fi
else
echo "Docker image is still used by other containers, skipping image removal: ${OPENRESTY_DOCKER_IMAGE}"
fi
fi
echo "Docker OpenResty cleanup complete."
elif [[ "$OPENRESTY_MODE" == "local" ]]; then
echo "Detected local OpenResty mode via openresty_path:"
echo " ${OPENRESTY_PATH}"
echo "Agent has been removed, but the local OpenResty installation was not modified."
echo "Please uninstall the local OpenResty manually if you no longer need it."
else
echo "OpenResty mode could not be determined because ${CONFIG_FILE} was not found before uninstall."
echo "If you were using Docker OpenResty, please remove its container and image manually if needed."
fi
echo ""
echo "OpenFlare Agent uninstall finished." echo "OpenFlare Agent uninstall finished."