diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a192236b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.idea +anubis-source +**/node_modules +**/.next +**/build +**/dist +**/.cache +**/coverage +**/*.db +**/*.log diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index aefa4062..80770c01 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -179,3 +179,166 @@ jobs: - name: Inspect image 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}" diff --git a/openflare_agent/Dockerfile b/openflare_agent/Dockerfile new file mode 100644 index 00000000..9bb6d334 --- /dev/null +++ b/openflare_agent/Dockerfile @@ -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"] diff --git a/openflare_agent/agent.json b/openflare_agent/agent.json index 524f723d..e4808bfd 100644 --- a/openflare_agent/agent.json +++ b/openflare_agent/agent.json @@ -2,8 +2,7 @@ "server_url": "http://127.0.0.1:3000", "agent_token": "373956188ddead1df6dd7c86cd330b73", "data_dir": "./data", - "openresty_container_name": "openflare-openresty", - "openresty_docker_image": "openresty/openresty:alpine", + "openresty_path": "openresty", "heartbeat_interval": 10000, "request_timeout": 10000 } diff --git a/openflare_agent/cmd/agent/main.go b/openflare_agent/cmd/agent/main.go index ef548579..778eb5fa 100644 --- a/openflare_agent/cmd/agent/main.go +++ b/openflare_agent/cmd/agent/main.go @@ -34,9 +34,6 @@ func main() { context.Background(), nginx.ExecutorOptions{ NginxPath: cfg.OpenrestyPath, - DockerBinary: cfg.DockerBinary, - ContainerName: cfg.OpenrestyContainerName, - Image: cfg.OpenrestyDockerImage, MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, CertDir: cfg.CertDir, @@ -52,33 +49,29 @@ func main() { "ip", cfg.NodeIP, "heartbeat_interval", cfg.HeartbeatInterval, "route_config", cfg.RouteConfigPath, + "access_log", cfg.AccessLogPath, "cert_dir", cfg.CertDir, "lua_dir", cfg.LuaDir, + "runtime_config_dir", cfg.RuntimeConfigDir, ) client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration()) stateStore := state.NewStore(cfg.StatePath) observabilityBuffer := state.NewObservabilityBufferStore(cfg.ObservabilityBufferPath) - runtimeRouteConfigPath := cfg.RouteConfigPath - if cfg.OpenrestyPath == "" { - runtimeRouteConfigPath = nginx.DockerRouteConfigPath - } runtimeManager := &nginx.Manager{ MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, - RuntimeRouteConfigPath: runtimeRouteConfigPath, + AccessLogPath: cfg.AccessLogPath, CertDir: cfg.CertDir, NginxCertDir: cfg.OpenrestyCertDir, LuaDir: cfg.LuaDir, NginxLuaDir: cfg.OpenrestyLuaDir, + RuntimeConfigDir: cfg.RuntimeConfigDir, OpenrestyObservabilityListen: nginx.ObservabilityListenAddress(cfg.OpenrestyPath, cfg.OpenrestyObservabilityPort), OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, OpenrestyResolverDirective: "", Executor: nginx.NewExecutor(nginx.ExecutorOptions{ NginxPath: cfg.OpenrestyPath, - DockerBinary: cfg.DockerBinary, - ContainerName: cfg.OpenrestyContainerName, - Image: cfg.OpenrestyDockerImage, MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, CertDir: cfg.CertDir, diff --git a/openflare_agent/internal/agent/runner_test.go b/openflare_agent/internal/agent/runner_test.go index cbdf03d6..2884be0a 100644 --- a/openflare_agent/internal/agent/runner_test.go +++ b/openflare_agent/internal/agent/runner_test.go @@ -302,15 +302,16 @@ func TestRunnerHeartbeatPayloadIncludesObservabilityExtensions(t *testing.T) { NginxVersion: "1.27.1.2", DataDir: tempDir, RouteConfigPath: filepath.Join(tempDir, "conf.d", "openflare_routes.conf"), + AccessLogPath: filepath.Join(tempDir, "var", "log", "openflare", "access.log"), HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond), }, StateStore: stateStore, } - if err := os.MkdirAll(filepath.Dir(runner.Config.RouteConfigPath), 0o755); err != nil { - t.Fatalf("failed to prepare route config dir: %v", err) + if err := os.MkdirAll(filepath.Dir(runner.Config.AccessLogPath), 0o755); err != nil { + t.Fatalf("failed to prepare access log dir: %v", err) } 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"), 0o644, ); err != nil { diff --git a/openflare_agent/internal/config/config.go b/openflare_agent/internal/config/config.go index 0397e055..ae42307b 100644 --- a/openflare_agent/internal/config/config.go +++ b/openflare_agent/internal/config/config.go @@ -3,24 +3,26 @@ package config import ( "encoding/json" "errors" + "fmt" "net" "openflare/utils/geoip/iputil" "os" pathpkg "path" "path/filepath" + "strconv" "strings" "time" ) const ( - defaultDockerMainConfigRelativePath = "etc/nginx/nginx.conf" - defaultDockerRouteConfigRelativePath = "etc/nginx/conf.d/openflare_routes.conf" + defaultMainConfigRelativePath = "etc/nginx/nginx.conf" + defaultRouteConfigRelativePath = "etc/nginx/conf.d/openflare_routes.conf" defaultCertDirRelativePath = "etc/nginx/certs" 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" - defaultDockerOpenRestyCertDir = "/etc/nginx/openflare-certs" - defaultDockerOpenRestyLuaDir = "/etc/nginx/openflare-lua" defaultOpenRestyObservabilityPort = 18081 defaultObservabilityReplayMinutes = 15 ) @@ -35,16 +37,18 @@ type Config struct { NginxVersion string `json:"-"` OpenrestyPath string `json:"openresty_path"` OpenrestyResolvers []string `json:"openresty_resolvers,omitempty"` - OpenrestyContainerName string `json:"openresty_container_name"` - OpenrestyDockerImage string `json:"openresty_docker_image"` - DockerBinary string `json:"docker_binary"` + OpenrestyContainerName string `json:"openresty_container_name,omitempty"` + OpenrestyDockerImage string `json:"openresty_docker_image,omitempty"` + DockerBinary string `json:"docker_binary,omitempty"` DataDir string `json:"data_dir"` MainConfigPath string `json:"main_config_path"` RouteConfigPath string `json:"route_config_path"` + AccessLogPath string `json:"access_log_path"` CertDir string `json:"cert_dir"` OpenrestyCertDir string `json:"openresty_cert_dir"` LuaDir string `json:"lua_dir"` OpenrestyLuaDir string `json:"openresty_lua_dir"` + RuntimeConfigDir string `json:"runtime_config_dir"` OpenrestyObservabilityPort int `json:"openresty_observability_port"` ObservabilityBufferPath string `json:"observability_buffer_path"` ObservabilityReplayMinutes int `json:"observability_replay_minutes"` @@ -68,10 +72,12 @@ type configFile struct { DataDir string `json:"data_dir"` MainConfigPath string `json:"main_config_path"` RouteConfigPath string `json:"route_config_path"` + AccessLogPath string `json:"access_log_path"` CertDir string `json:"cert_dir"` OpenrestyCertDir string `json:"openresty_cert_dir"` LuaDir string `json:"lua_dir"` OpenrestyLuaDir string `json:"openresty_lua_dir"` + RuntimeConfigDir string `json:"runtime_config_dir"` OpenrestyObservabilityPort int `json:"openresty_observability_port"` ObservabilityBufferPath string `json:"observability_buffer_path"` ObservabilityReplayMinutes int `json:"observability_replay_minutes"` @@ -82,11 +88,16 @@ type configFile struct { func Load(path string) (*Config, error) { data, err := os.ReadFile(path) - if err != nil { + if err != nil && !os.IsNotExist(err) { return nil, err } 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 } cfg := &Config{ @@ -103,10 +114,12 @@ func Load(path string) (*Config, error) { DataDir: file.DataDir, MainConfigPath: file.MainConfigPath, RouteConfigPath: file.RouteConfigPath, + AccessLogPath: file.AccessLogPath, CertDir: file.CertDir, OpenrestyCertDir: file.OpenrestyCertDir, LuaDir: file.LuaDir, OpenrestyLuaDir: file.OpenrestyLuaDir, + RuntimeConfigDir: file.RuntimeConfigDir, OpenrestyObservabilityPort: file.OpenrestyObservabilityPort, ObservabilityBufferPath: file.ObservabilityBufferPath, ObservabilityReplayMinutes: file.ObservabilityReplayMinutes, @@ -115,6 +128,7 @@ func Load(path string) (*Config, error) { RequestTimeout: file.RequestTimeout, } cfg.configPath = path + applyEnvOverrides(cfg) applyDefaults(cfg, filepath.Dir(path)) if err = validate(cfg); err != nil { return nil, err @@ -126,14 +140,8 @@ func applyDefaults(cfg *Config, baseDir string) { baseDir = filepath.Clean(baseDir) cfg.AgentVersion = AgentVersion cfg.OpenrestyResolvers = normalizeResolverList(cfg.OpenrestyResolvers) - if cfg.OpenrestyContainerName == "" { - cfg.OpenrestyContainerName = "openflare-openresty" - } - if cfg.OpenrestyDockerImage == "" { - cfg.OpenrestyDockerImage = "openresty/openresty:alpine" - } - if cfg.DockerBinary == "" { - cfg.DockerBinary = "docker" + if cfg.OpenrestyPath == "" { + cfg.OpenrestyPath = "openresty" } if cfg.DataDir == "" { cfg.DataDir = filepath.Join(baseDir, "data") @@ -144,40 +152,32 @@ func applyDefaults(cfg *Config, baseDir string) { if cfg.NodeIP == "" { cfg.NodeIP = detectNodeIP() } - if cfg.OpenrestyPath == "" { - cfg.MainConfigPath = joinManagedPath(cfg.DataDir, defaultDockerMainConfigRelativePath) - cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultDockerRouteConfigRelativePath) - cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath) - } else { - if cfg.MainConfigPath == "" { - cfg.MainConfigPath = joinManagedPath(cfg.DataDir, defaultDockerMainConfigRelativePath) - } - if cfg.RouteConfigPath == "" { - cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultDockerRouteConfigRelativePath) - } - if cfg.StatePath == "" { - cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath) - } + if cfg.MainConfigPath == "" { + cfg.MainConfigPath = joinManagedPath(cfg.DataDir, defaultMainConfigRelativePath) + } + if cfg.RouteConfigPath == "" { + cfg.RouteConfigPath = joinManagedPath(cfg.DataDir, defaultRouteConfigRelativePath) + } + if cfg.AccessLogPath == "" { + cfg.AccessLogPath = joinManagedPath(cfg.DataDir, defaultAccessLogRelativePath) + } + if cfg.StatePath == "" { + cfg.StatePath = joinManagedPath(cfg.DataDir, defaultStateRelativePath) } if cfg.CertDir == "" { cfg.CertDir = joinManagedPath(cfg.DataDir, defaultCertDirRelativePath) } if cfg.OpenrestyCertDir == "" { - if cfg.OpenrestyPath != "" { - cfg.OpenrestyCertDir = cfg.CertDir - } else { - cfg.OpenrestyCertDir = defaultDockerOpenRestyCertDir - } + cfg.OpenrestyCertDir = cfg.CertDir } if cfg.LuaDir == "" { cfg.LuaDir = joinManagedPath(cfg.DataDir, defaultLuaDirRelativePath) } if cfg.OpenrestyLuaDir == "" { - if cfg.OpenrestyPath != "" { - cfg.OpenrestyLuaDir = cfg.LuaDir - } else { - cfg.OpenrestyLuaDir = defaultDockerOpenRestyLuaDir - } + cfg.OpenrestyLuaDir = cfg.LuaDir + } + if cfg.RuntimeConfigDir == "" { + cfg.RuntimeConfigDir = joinManagedPath(cfg.DataDir, defaultRuntimeConfigDirRelativePath) } if cfg.OpenrestyObservabilityPort <= 0 { cfg.OpenrestyObservabilityPort = defaultOpenRestyObservabilityPort @@ -210,6 +210,9 @@ func normalizeManagedPaths(cfg *Config) { if usesSlashPath(cfg.RouteConfigPath) { cfg.RouteConfigPath = filepath.ToSlash(cfg.RouteConfigPath) } + if usesSlashPath(cfg.AccessLogPath) { + cfg.AccessLogPath = filepath.ToSlash(cfg.AccessLogPath) + } if usesSlashPath(cfg.CertDir) { cfg.CertDir = filepath.ToSlash(cfg.CertDir) } @@ -222,6 +225,9 @@ func normalizeManagedPaths(cfg *Config) { if usesSlashPath(cfg.OpenrestyLuaDir) { cfg.OpenrestyLuaDir = filepath.ToSlash(cfg.OpenrestyLuaDir) } + if usesSlashPath(cfg.RuntimeConfigDir) { + cfg.RuntimeConfigDir = filepath.ToSlash(cfg.RuntimeConfigDir) + } if usesSlashPath(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 { return strings.HasPrefix(path, "/") } diff --git a/openflare_agent/internal/config/config_test.go b/openflare_agent/internal/config/config_test.go index 1bba6cb4..2264746f 100644 --- a/openflare_agent/internal/config/config_test.go +++ b/openflare_agent/internal/config/config_test.go @@ -9,7 +9,7 @@ import ( "time" ) -func TestLoadDockerModeUsesManagedPaths(t *testing.T) { +func TestLoadDefaultsToManagedBinaryPaths(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "agent.json") payload := map[string]any{ @@ -34,31 +34,34 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) { if cfg.DataDir != filepath.Join(dir, "data") { 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) } - 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) } + 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) { t.Fatalf("unexpected cert dir: %s", cfg.CertDir) } if cfg.LuaDir != filepath.Join(dir, "data", defaultLuaDirRelativePath) { t.Fatalf("unexpected lua dir: %s", cfg.LuaDir) } - if cfg.OpenrestyContainerName != "openflare-openresty" { - t.Fatalf("unexpected openresty container name: %s", cfg.OpenrestyContainerName) + if cfg.RuntimeConfigDir != filepath.Join(dir, "data", defaultRuntimeConfigDirRelativePath) { + t.Fatalf("unexpected runtime config dir: %s", cfg.RuntimeConfigDir) } - if cfg.OpenrestyDockerImage != "openresty/openresty:alpine" { - t.Fatalf("unexpected openresty image: %s", cfg.OpenrestyDockerImage) - } - if cfg.OpenrestyCertDir != defaultDockerOpenRestyCertDir { + if cfg.OpenrestyCertDir != cfg.CertDir { 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) } - if cfg.StatePath != filepath.Join(dir, "data", defaultDockerStateRelativePath) { + if cfg.StatePath != filepath.Join(dir, "data", defaultStateRelativePath) { t.Fatalf("unexpected state path: %s", cfg.StatePath) } 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) { dir := t.TempDir() configPath := filepath.Join(dir, "agent.json") @@ -178,13 +216,16 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { 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) } - if cfg.MainConfigPath != "/srv/openflare/"+defaultDockerMainConfigRelativePath { + if cfg.MainConfigPath != "/srv/openflare/"+defaultMainConfigRelativePath { 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) } if cfg.ObservabilityBufferPath != "/srv/openflare/"+defaultObservabilityBufferRelativePath { @@ -196,6 +237,70 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { if cfg.LuaDir != "/srv/openflare/"+defaultLuaDirRelativePath { 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) { @@ -283,6 +388,15 @@ func TestSavePersistsMillisecondsAndOmitsRuntimeVersions(t *testing.T) { if _, ok := decoded["nginx_path"]; ok { 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) { diff --git a/openflare_agent/internal/nginx/manager.go b/openflare_agent/internal/nginx/manager.go index 445010a2..10976714 100644 --- a/openflare_agent/internal/nginx/manager.go +++ b/openflare_agent/internal/nginx/manager.go @@ -24,15 +24,11 @@ const CertDirPlaceholder = "__OPENFLARE_CERT_DIR__" const RouteConfigPlaceholder = "__OPENFLARE_ROUTE_CONFIG__" const AccessLogPlaceholder = "__OPENFLARE_ACCESS_LOG__" const LuaDirPlaceholder = "__OPENFLARE_LUA_DIR__" +const RuntimeConfigDirPlaceholder = "__OPENFLARE_RUNTIME_CONFIG_DIR__" const ObservabilityListenPlaceholder = "__OPENFLARE_OBSERVABILITY_LISTEN__" const ObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__" const ResolverDirectivePlaceholder = "__OPENFLARE_RESOLVER_DIRECTIVE__" 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 { Test(ctx context.Context) error @@ -55,13 +51,14 @@ func (r *OSCommandRunner) Run(ctx context.Context, name string, args ...string) } type PathExecutor struct { - Path string - Runner CommandRunner + Path string + ConfigPath string + Runner CommandRunner } func (e *PathExecutor) Test(ctx context.Context) error { - slog.Debug("running openresty test with binary", "path", e.Path) - output, err := e.Runner.Run(ctx, e.Path, "-t") + slog.Debug("running openresty test with binary", "path", e.Path, "config", e.ConfigPath) + output, err := e.Runner.Run(ctx, e.Path, "-t", "-c", e.ConfigPath) if err != nil { 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 { - slog.Debug("running openresty reload with binary", "path", e.Path) - output, err := e.Runner.Run(ctx, e.Path, "-s", "reload") + slog.Debug("running openresty reload with binary", "path", e.Path, "config", e.ConfigPath) + output, err := e.Runner.Run(ctx, e.Path, "-s", "reload", "-c", e.ConfigPath) 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)) } 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 { - return nil + if err := e.Test(ctx); err != nil { + return err + } + return e.Reload(ctx) } 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 { - slog.Info("restarting openresty with binary", "path", e.Path) - output, err := e.Runner.Run(ctx, e.Path, "-s", "quit") + slog.Info("restarting openresty with binary", "path", e.Path, "config", e.ConfigPath) + output, err := e.Runner.Run(ctx, e.Path, "-s", "quit", "-c", e.ConfigPath) if err != nil { text := string(output) if !isIgnorableOpenrestyStopError(text) { return fmt.Errorf("openresty stop failed: %w: %s", err, text) } } - output, err = e.Runner.Run(ctx, e.Path) + output, err = e.Runner.Run(ctx, e.Path, "-c", e.ConfigPath) if err != nil { 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 } -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 { MainConfigPath string RouteConfigPath string - RuntimeRouteConfigPath string + AccessLogPath string CertDir string NginxCertDir string LuaDir string NginxLuaDir string + RuntimeConfigDir string OpenrestyObservabilityListen string OpenrestyObservabilityPort int OpenrestyResolverDirective string @@ -419,8 +184,8 @@ func (m *Manager) activateConfig(ctx context.Context) error { if m.Executor == nil { return errors.New("executor 未配置") } - if _, ok := m.Executor.(*DockerExecutor); ok { - return m.Executor.EnsureRuntime(ctx, true) + if err := m.Executor.Test(ctx); err != nil { + return err } return m.Executor.Reload(ctx) } @@ -455,14 +220,7 @@ func (m *Manager) EnsureLuaAssets() error { if strings.TrimSpace(m.LuaDir) == "" { return nil } - allSupportFiles := append(ManagedObservabilityLuaFiles(), ManagedPowLuaFiles()...) - existingPowConfig, err := m.readPowConfigFile() - if err != nil { - return err - } - if existingPowConfig != nil { - allSupportFiles = append(allSupportFiles, *existingPowConfig) - } + allSupportFiles := append(ManagedObservabilityLuaFiles(), m.managedPowLuaFiles()...) powStaticFiles, err := ManagedPowStaticFiles() if err != nil { return fmt.Errorf("load pow static files: %w", err) @@ -569,9 +327,6 @@ func (m *Manager) CurrentChecksum() (string, error) { type ExecutorOptions struct { NginxPath string - DockerBinary string - ContainerName string - Image string MainConfigPath string RouteConfigPath string CertDir string @@ -583,48 +338,10 @@ type ExecutorOptions struct { func NewExecutor(options ExecutorOptions) Executor { runner := &OSCommandRunner{} - if options.NginxPath != "" { - return &PathExecutor{ - Path: options.NginxPath, - Runner: runner, - } - } - mainConfigPath := options.MainConfigPath - if mainConfigPath != "" { - if absPath, err := filepath.Abs(mainConfigPath); err == nil { - mainConfigPath = absPath - } - } - routeConfigDir := filepath.Dir(options.RouteConfigPath) - if options.RouteConfigPath != "" { - if absDir, err := filepath.Abs(routeConfigDir); err == nil { - routeConfigDir = absDir - } - } - certDir := options.CertDir - if certDir != "" { - if absDir, err := filepath.Abs(certDir); err == nil { - certDir = absDir - } - } - luaDir := options.LuaDir - if luaDir != "" { - if absDir, err := filepath.Abs(luaDir); err == nil { - luaDir = absDir - } - } - return &DockerExecutor{ - DockerBinary: options.DockerBinary, - ContainerName: options.ContainerName, - Image: options.Image, - MainConfigPath: mainConfigPath, - RouteConfigDir: routeConfigDir, - CertDir: certDir, - NginxCertDir: options.NginxCertDir, - LuaDir: luaDir, - NginxLuaDir: options.NginxLuaDir, - OpenrestyObservabilityPort: options.OpenrestyObservabilityPort, - Runner: runner, + return &PathExecutor{ + Path: strings.TrimSpace(options.NginxPath), + ConfigPath: strings.TrimSpace(options.MainConfigPath), + Runner: runner, } } @@ -653,15 +370,7 @@ func detectVersion(ctx context.Context, options ExecutorOptions, runner CommandR } return version, nil } - output, err := runDockerVersionProbe(ctx, runner, options.DockerBinary, options.Image) - if err != nil { - return "", fmt.Errorf("run docker %s -v failed: %w: %s", dockerRuntimeCommand, err, string(output)) - } - version := parseNginxVersion(string(output)) - if version == "" { - return "", errors.New("cannot parse runtime version from docker output") - } - return version, nil + return "", errors.New("openresty path is empty") } 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") } -func (e *DockerExecutor) runEphemeralRuntimeCommand(ctx context.Context, args ...string) ([]byte, error) { - return e.runEphemeralRuntimeCommandWithBinary(ctx, dockerRuntimeCommand, args...) -} - -func (e *DockerExecutor) runEphemeralRuntimeCommandWithBinary(ctx context.Context, runtimeBinary string, args ...string) ([]byte, error) { - runtimeArgs := []string{ - "run", - "--rm", - "-v", - fmt.Sprintf("%s:%s", e.MainConfigPath, DockerMainConfigPath), - "-v", - fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir), - "-v", - fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir), - "-v", - fmt.Sprintf("%s:%s", e.LuaDir, e.NginxLuaDir), - e.Image, - runtimeBinary, +func isOpenrestyNotRunningError(output string) bool { + text := strings.ToLower(strings.TrimSpace(output)) + if text == "" { + return false } - runtimeArgs = append(runtimeArgs, args...) - return e.Runner.Run(ctx, e.DockerBinary, runtimeArgs...) -} - -func runDockerVersionProbe(ctx context.Context, runner CommandRunner, dockerBinary string, image string) ([]byte, error) { - return runner.Run(ctx, dockerBinary, "run", "--rm", image, dockerRuntimeCommand, "-v") + return strings.Contains(text, "invalid pid") || + strings.Contains(text, "no such process") || + strings.Contains(text, "open()") && strings.Contains(text, "nginx.pid") && strings.Contains(text, "failed") } type backupState struct { @@ -737,11 +429,21 @@ func (m *Manager) backup() (*backupState, error) { if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil { return nil, err } + if m.AccessLogPath != "" { + if err := os.MkdirAll(filepath.Dir(m.AccessLogPath), 0o755); err != nil { + return nil, err + } + } if m.CertDir != "" { if err := os.MkdirAll(m.CertDir, 0o755); err != nil { return nil, err } } + if m.RuntimeConfigDir != "" { + if err := os.MkdirAll(m.RuntimeConfigDir, 0o755); err != nil { + return nil, err + } + } state := &backupState{} mainData, err := os.ReadFile(m.MainConfigPath) if err == nil { @@ -806,10 +508,10 @@ func (m *Manager) writeCertFiles(certFiles []protocol.SupportFile) error { } func (m *Manager) writePowConfig(supportFiles []protocol.SupportFile) error { - if m.LuaDir == "" { + if m.RuntimeConfigDir == "" { return nil } - configPath := filepath.Join(m.LuaDir, "pow_config.json") + configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json") for _, file := range supportFiles { if file.Path == "pow_config.json" { 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) { 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 } func (m *Manager) writeManagedCertFiles(certFiles []protocol.SupportFile) error { files := make([]managedFile, 0, len(certFiles)) for _, file := range certFiles { + if file.Path == "pow_config.json" { + continue + } targetPath, err := m.certFileTargetPath(file.Path) if err != nil { return err @@ -863,11 +574,14 @@ func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) { if info.IsDir() { return nil } - data, err := os.ReadFile(path) + relativePath, err := filepath.Rel(m.CertDir, path) if err != nil { 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 { return err } @@ -887,10 +601,10 @@ func (m *Manager) readCertFiles() ([]protocol.SupportFile, error) { } func (m *Manager) readPowConfigFile() (*protocol.SupportFile, error) { - if m.LuaDir == "" { + if m.RuntimeConfigDir == "" { return nil, nil } - configPath := filepath.Join(m.LuaDir, "pow_config.json") + configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json") data, err := os.ReadFile(configPath) if err != nil { if os.IsNotExist(err) { @@ -920,10 +634,10 @@ func (m *Manager) readManagedSupportFiles() ([]protocol.SupportFile, error) { } func (m *Manager) restorePowConfig(state *backupState) error { - if state == nil || m.LuaDir == "" { + if state == nil || m.RuntimeConfigDir == "" { return nil } - configPath := filepath.Join(m.LuaDir, "pow_config.json") + configPath := filepath.Join(m.RuntimeConfigDir, "pow_config.json") if state.PowConfig == nil { if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) { return err @@ -933,6 +647,16 @@ func (m *Manager) restorePowConfig(state *backupState) error { 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) { if strings.TrimSpace(m.CertDir) == "" { return "", errors.New("cert dir 不能为空") @@ -1119,14 +843,20 @@ func (m *Manager) renderMainConfig(content string) string { 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 { if port <= 0 { return "" } - if strings.TrimSpace(openrestyPath) != "" { - return fmt.Sprintf("127.0.0.1:%d", port) - } - return fmt.Sprintf("%d", port) + return fmt.Sprintf("127.0.0.1:%d", port) } func ResolverDirective(openrestyPath string, explicitResolvers []string) string { @@ -1145,7 +875,7 @@ func resolverAddresses(openrestyPath string, explicitResolvers []string) []strin if err != nil { return nil } - return parseResolverAddresses(string(data), strings.TrimSpace(openrestyPath) == "") + return parseResolverAddresses(string(data), false) } func parseResolverAddresses(content string, dockerMode bool) []string { @@ -1213,18 +943,11 @@ func RequiresRuntimeResolver(originURL string) bool { } func (m *Manager) routeConfigIncludePath() string { - if strings.TrimSpace(m.RuntimeRouteConfigPath) != "" { - return strings.TrimSpace(m.RuntimeRouteConfigPath) - } return strings.TrimSpace(m.RouteConfigPath) } func (m *Manager) accessLogRuntimePath() string { - includePath := m.routeConfigIncludePath() - if strings.TrimSpace(includePath) == "" { - return "" - } - return filepath.ToSlash(filepath.Join(filepath.Dir(includePath), "openflare_access.log")) + return filepath.ToSlash(strings.TrimSpace(m.AccessLogPath)) } func (m *Manager) luaRuntimePath() string { diff --git a/openflare_agent/internal/nginx/manager_test.go b/openflare_agent/internal/nginx/manager_test.go index 2bd460b6..b2799f0f 100644 --- a/openflare_agent/internal/nginx/manager_test.go +++ b/openflare_agent/internal/nginx/manager_test.go @@ -89,8 +89,9 @@ func (e *scriptedExecutor) Restart(ctx context.Context) error { func TestPathExecutorCommands(t *testing.T) { runner := &fakeRunner{} executor := &PathExecutor{ - Path: "/usr/local/openresty/nginx/sbin/openresty", - Runner: runner, + Path: "/usr/local/openresty/nginx/sbin/openresty", + ConfigPath: "/data/etc/nginx/nginx.conf", + Runner: runner, } if err := executor.Test(context.Background()); err != nil { @@ -101,8 +102,8 @@ func TestPathExecutorCommands(t *testing.T) { } expected := []runCall{ - {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-t"}}, - {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload"}}, + {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", "-c", "/data/etc/nginx/nginx.conf"}}, } if !reflect.DeepEqual(runner.calls, expected) { t.Fatalf("unexpected calls: %#v", runner.calls) @@ -110,13 +111,18 @@ func TestPathExecutorCommands(t *testing.T) { } func TestPathExecutorEnsureRuntimeNoop(t *testing.T) { + runner := &fakeRunner{} executor := &PathExecutor{ - Path: "/usr/local/openresty/nginx/sbin/openresty", - Runner: &fakeRunner{}, + Path: "/usr/local/openresty/nginx/sbin/openresty", + ConfigPath: "/data/etc/nginx/nginx.conf", + Runner: runner, } if err := executor.EnsureRuntime(context.Background(), true); err != nil { 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) { @@ -129,8 +135,9 @@ func TestPathExecutorRestartIgnoresMissingPID(t *testing.T) { }, } executor := &PathExecutor{ - Path: "/usr/local/openresty/nginx/sbin/openresty", - Runner: runner, + Path: "/usr/local/openresty/nginx/sbin/openresty", + ConfigPath: "/data/etc/nginx/nginx.conf", + Runner: runner, } if err := executor.Restart(context.Background()); err != nil { 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{ runFn: func(name string, args ...string) ([]byte, error) { - if len(args) >= 4 && args[0] == "inspect" && args[2] == "{{.State.Running}}" { - return []byte("false"), nil + if len(args) >= 2 && args[0] == "-s" && args[1] == "reload" { + 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("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 + return []byte(""), nil }, } - executor := &DockerExecutor{ - DockerBinary: "docker", - ContainerName: "openflare-openresty", - Image: "openresty/openresty:alpine", - MainConfigPath: filepath.Clean("/tmp/nginx.conf"), - RouteConfigDir: filepath.Clean("/tmp/routes"), - CertDir: filepath.Clean("/tmp/certs"), - NginxCertDir: "/etc/nginx/openflare-certs", - LuaDir: filepath.Clean("/tmp/lua"), - NginxLuaDir: "/etc/nginx/openflare-lua", - Runner: runner, + executor := &PathExecutor{ + Path: "/usr/local/openresty/nginx/sbin/openresty", + ConfigPath: "/data/etc/nginx/nginx.conf", + 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 { 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{ - {name: "docker", args: []string{"inspect", "-f", "{{.State.Running}}", "openflare-openresty"}}, - {name: "docker", args: []string{"exec", "openflare-openresty", "openresty", "-s", "reload"}}, + {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-s", "reload", "-c", "/data/etc/nginx/nginx.conf"}}, + {name: "/usr/local/openresty/nginx/sbin/openresty", args: []string{"-c", "/data/etc/nginx/nginx.conf"}}, } if !reflect.DeepEqual(runner.calls, expected) { 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) { version, err := detectVersion(context.Background(), ExecutorOptions{ NginxPath: "/usr/local/openresty/nginx/sbin/openresty", @@ -578,9 +194,11 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { mainPath := filepath.Join(tempDir, "nginx.conf") routePath := filepath.Join(tempDir, "conf.d", "openflare_routes.conf") certDir := filepath.Join(tempDir, "certs") + accessLogPath := filepath.Join(tempDir, "var", "log", "openflare", "access.log") manager := &Manager{ MainConfigPath: mainPath, RouteConfigPath: routePath, + AccessLogPath: accessLogPath, CertDir: certDir, NginxCertDir: "/etc/nginx/openflare-certs", LuaDir: filepath.Join(tempDir, "lua"), @@ -602,7 +220,7 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { if err != nil { 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 { 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) { 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?)", @@ -904,8 +455,9 @@ func TestCertFileMode(t *testing.T) { func TestManagerEnsureLuaAssetsWritesReadableFiles(t *testing.T) { tempDir := t.TempDir() manager := &Manager{ - LuaDir: filepath.Join(tempDir, "lua"), - NginxLuaDir: "/etc/nginx/openflare-lua", + LuaDir: filepath.Join(tempDir, "lua"), + NginxLuaDir: "/etc/nginx/openflare-lua", + RuntimeConfigDir: filepath.Join(tempDir, "runtime"), } 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 { 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() 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) } - powConfigPath := filepath.Join(luaDir, "pow_config.json") + powConfigPath := filepath.Join(runtimeConfigDir, "pow_config.json") want := `[{"domains":["pow.example.com"],"enabled":true}]` if err := os.WriteFile(powConfigPath, []byte(want), 0o644); err != nil { t.Fatalf("WriteFile failed: %v", err) } - manager := &Manager{LuaDir: luaDir} + manager := &Manager{LuaDir: luaDir, RuntimeConfigDir: runtimeConfigDir} if err := manager.EnsureLuaAssets(); err != nil { t.Fatalf("EnsureLuaAssets failed: %v", err) @@ -949,6 +509,52 @@ func TestEnsureLuaAssetsPreservesPowConfig(t *testing.T) { if 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) { @@ -956,12 +562,14 @@ func TestManagerCurrentChecksumIncludesPowConfig(t *testing.T) { mainPath := filepath.Join(tempDir, "nginx.conf") routePath := filepath.Join(tempDir, "routes.conf") luaDir := filepath.Join(tempDir, "lua") + runtimeConfigDir := filepath.Join(tempDir, "runtime") manager := &Manager{ - MainConfigPath: mainPath, - RouteConfigPath: routePath, - LuaDir: luaDir, - NginxLuaDir: "/etc/nginx/openflare-lua", - Executor: &fakeExecutor{}, + MainConfigPath: mainPath, + RouteConfigPath: routePath, + LuaDir: luaDir, + NginxLuaDir: "/etc/nginx/openflare-lua", + RuntimeConfigDir: runtimeConfigDir, + Executor: &fakeExecutor{}, } outcome := manager.Apply( @@ -1189,8 +797,8 @@ func TestManagerApplyRejectsCertFilePathTraversal(t *testing.T) { } func TestObservabilityListenAddress(t *testing.T) { - if got := ObservabilityListenAddress("", 18081); got != "18081" { - t.Fatalf("unexpected docker observability listen address: %s", got) + if got := ObservabilityListenAddress("", 18081); got != "127.0.0.1:18081" { + 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" { t.Fatalf("unexpected path observability listen address: %s", got) diff --git a/openflare_agent/internal/nginx/pow_assets.go b/openflare_agent/internal/nginx/pow_assets.go index b41d16aa..1da242aa 100644 --- a/openflare_agent/internal/nginx/pow_assets.go +++ b/openflare_agent/internal/nginx/pow_assets.go @@ -36,7 +36,7 @@ end -- Lazy-load pow_config from file; reload when content changes local function load_pow_config() 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", "/usr/local/openresty/nginx/conf/pow_config.json" } diff --git a/openflare_agent/internal/observability/traffic.go b/openflare_agent/internal/observability/traffic.go index 50a3a4da..1f3be468 100644 --- a/openflare_agent/internal/observability/traffic.go +++ b/openflare_agent/internal/observability/traffic.go @@ -9,7 +9,6 @@ import ( "openflare-agent/internal/protocol" "openflare-agent/internal/state" "os" - "path/filepath" "regexp" "sort" "strconv" @@ -128,10 +127,10 @@ func readAccessLogDelta(cfg *config.Config, stateStore *state.Store) *trafficAgg } func managedAccessLogPath(cfg *config.Config) string { - if cfg == nil || strings.TrimSpace(cfg.RouteConfigPath) == "" { + if cfg == nil || strings.TrimSpace(cfg.AccessLogPath) == "" { return "" } - return filepath.Join(filepath.Dir(cfg.RouteConfigPath), "openflare_access.log") + return cfg.AccessLogPath } func newTrafficAggregate() *trafficAggregate { diff --git a/openflare_agent/internal/observability/traffic_test.go b/openflare_agent/internal/observability/traffic_test.go index c448f7de..33f47861 100644 --- a/openflare_agent/internal/observability/traffic_test.go +++ b/openflare_agent/internal/observability/traffic_test.go @@ -28,7 +28,7 @@ func TestBuildTrafficReportAggregatesManagedAccessLog(t *testing.T) { } 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 { t.Fatal("expected traffic report") } @@ -50,7 +50,7 @@ func TestBuildTrafficReportAggregatesManagedAccessLog(t *testing.T) { 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 { 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) } - report := BuildTrafficReport(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) + report := BuildTrafficReport(&config.Config{AccessLogPath: logPath}, stateStore, nil) if report == nil || report.RequestCount != 1 { 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")) - 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 { 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")) - _, accessLogs, _ := BuildTrafficObservability(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) + _, accessLogs, _ := BuildTrafficObservability(&config.Config{AccessLogPath: logPath}, stateStore, nil) if len(accessLogs) != 1 { 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")) - report := BuildTrafficReport(&config.Config{RouteConfigPath: routeConfigPath}, stateStore, nil) + report := BuildTrafficReport(&config.Config{AccessLogPath: logPath}, stateStore, nil) if report == nil { t.Fatal("expected traffic report from combined access log") } diff --git a/scripts/install-agent.sh b/scripts/install-agent.sh index 9710487e..1c9583d0 100755 --- a/scripts/install-agent.sh +++ b/scripts/install-agent.sh @@ -14,6 +14,7 @@ DISCOVERY_TOKEN="" AGENT_TOKEN="" CREATE_SERVICE="true" SERVICE_NAME="openflare-agent" +OPENRESTY_PATH="" usage() { cat </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}" echo "Detected platform: ${OS}/${ARCH}" @@ -140,9 +157,9 @@ if [[ -n "$AGENT_TOKEN" ]]; then { "server_url": "${SERVER_URL}", "agent_token": "${AGENT_TOKEN}", + "openresty_path": "${OPENRESTY_PATH}", "data_dir": "${INSTALL_DIR}/data", "heartbeat_interval": 30000, - "sync_interval": 30000, "request_timeout": 10000 } CFGEOF @@ -151,9 +168,9 @@ else { "server_url": "${SERVER_URL}", "discovery_token": "${DISCOVERY_TOKEN}", + "openresty_path": "${OPENRESTY_PATH}", "data_dir": "${INSTALL_DIR}/data", "heartbeat_interval": 30000, - "sync_interval": 30000, "request_timeout": 10000 } CFGEOF @@ -197,3 +214,4 @@ echo "OpenFlare Agent installed successfully!" echo " Binary: ${INSTALL_DIR}/openflare-agent" echo " Config: ${CONFIG_FILE}" echo " Data: ${INSTALL_DIR}/data" +echo " OpenResty: ${OPENRESTY_PATH}" diff --git a/scripts/uninstall-agent.sh b/scripts/uninstall-agent.sh index 0da898d8..63577646 100644 --- a/scripts/uninstall-agent.sh +++ b/scripts/uninstall-agent.sh @@ -19,9 +19,7 @@ Options: Behavior: 1. Stop the agent service/process and remove the entire installation directory 2. Remove the systemd service definition when present - 3. Check the saved agent config to identify the OpenResty mode - 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 + 3. Leave the local OpenResty installation untouched Examples: uninstall-agent.sh @@ -44,52 +42,9 @@ if [[ -z "$INSTALL_DIR" || "$INSTALL_DIR" == "/" || "$INSTALL_DIR" == "." ]]; th exit 1 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" -CONFIG_FILE="${INSTALL_DIR}/agent.json" 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" if command -v systemctl >/dev/null 2>&1; then SYSTEMCTL_AVAILABLE="true" @@ -143,50 +98,5 @@ fi echo "Agent uninstall complete." echo "" -echo "Checking OpenResty installation mode..." - -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 "Local OpenResty was not modified. Remove it manually if you no longer need it." echo "OpenFlare Agent uninstall finished."