From 7953f657ccd09132d8db524235762f4666b8d8d7 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 20 Feb 2026 20:11:33 +0100 Subject: [PATCH] v0.21.2: Fix config apply on Docker bind mounts os.Rename() fails with "device or resource busy" on bind-mounted files. Fall back to direct os.WriteFile when rename fails. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 3 +++ controller/internal/api/router.go | 13 +++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5c9f4..4ed2321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Changelog +### v0.21.2 — Config Apply Bind Mount Fix (2026-02-20) +- **Fix config apply on Docker bind mounts**: `POST /api/config/apply` failed with "device or resource busy" because `os.Rename()` doesn't work on bind-mounted files. Now falls back to direct write when rename fails. + ### v0.21.1 — Config Content Endpoint (2026-02-20) - **`GET /api/config`**: New endpoint returning raw controller.yaml content (text/yaml). Used by Hub for live config diff and pull operations. Same auth as other config endpoints (Bearer token or session cookie). diff --git a/controller/internal/api/router.go b/controller/internal/api/router.go index 3966172..2bf474e 100644 --- a/controller/internal/api/router.go +++ b/controller/internal/api/router.go @@ -954,7 +954,8 @@ func (r *Router) configApply(w http.ResponseWriter, req *http.Request) { return } - // Atomic write: write to .tmp, then rename + // Write config: try atomic rename first, fall back to direct write + // (os.Rename fails on Docker bind mounts with "device or resource busy") tmpPath := r.configPath + ".tmp" if err := os.WriteFile(tmpPath, body, 0644); err != nil { r.logger.Printf("[ERROR] Config apply: failed to write temp file: %v", err) @@ -964,9 +965,13 @@ func (r *Router) configApply(w http.ResponseWriter, req *http.Request) { if err := os.Rename(tmpPath, r.configPath); err != nil { os.Remove(tmpPath) - r.logger.Printf("[ERROR] Config apply: failed to rename temp file: %v", err) - writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: "failed to apply config"}) - return + // Rename failed (likely Docker bind mount) — write directly + if err := os.WriteFile(r.configPath, body, 0644); err != nil { + r.logger.Printf("[ERROR] Config apply: failed to write config: %v", err) + writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: "failed to apply config"}) + return + } + r.logger.Printf("[API] Config apply: rename failed, wrote directly (bind mount)") } r.logger.Printf("[API] Config applied from Hub (%d bytes), restart needed to take effect", len(body))