diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b74fb..5f5c9f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Changelog +### 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). + ### What was just completed (2026-02-20 session 64) - **v0.21.0 — Hub Monitoring Takeover (Controller-side, Phases 5+6):** diff --git a/controller/README.md b/controller/README.md index 52b42fc..eb6f25c 100644 --- a/controller/README.md +++ b/controller/README.md @@ -1066,6 +1066,7 @@ Self-update endpoints accept session auth OR `Authorization: Bearer ` (same as self-update). The `/api/config/apply` endpoint: - Accepts raw YAML body (the generated config from Hub) @@ -1162,6 +1163,7 @@ See `docker-compose.yml` for the full volume configuration. - [x] Disaster recovery (v0.15.5) — Hub-based infra backup, auto-mount by UUID, restore UI with full-page takeover - [x] Controller self-update (v0.16.0) — Watchtower-style pull + restart, Settings page UI, API key auth, auto-update scheduling - [x] Hub-managed config (v0.20.0) — Config apply endpoint (`POST /api/config/apply`), config hash in reports for sync comparison +- [x] Config content endpoint (v0.21.1) — `GET /api/config` returns raw YAML for Hub live diff and pull operations ### In Progress / Planned diff --git a/controller/internal/api/router.go b/controller/internal/api/router.go index 2170c66..3966172 100644 --- a/controller/internal/api/router.go +++ b/controller/internal/api/router.go @@ -89,6 +89,10 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { case path == "/config/hash" && req.Method == http.MethodGet: r.configHash(w, req) + // GET /api/config — return raw controller.yaml content + case path == "/config" && req.Method == http.MethodGet: + r.configContent(w, req) + // GET /api/stacks/{name}/deploy-fields case hasSuffix(path, "/deploy-fields") && req.Method == http.MethodGet: r.getDeployFields(w, req, extractName(path, "/deploy-fields")) @@ -978,6 +982,16 @@ func (r *Router) configHash(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]string{"hash": hash, "path": filepath.Base(r.configPath)}}) } +func (r *Router) configContent(w http.ResponseWriter, _ *http.Request) { + data, err := os.ReadFile(r.configPath) + if err != nil { + writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: "failed to read config"}) + return + } + w.Header().Set("Content-Type", "text/yaml; charset=utf-8") + w.Write(data) +} + func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status)