Cluster UI: Blocklists management, locked config-file items, read-only upstreams; honour allow_config_write

The console now works as the cluster's editor, without the monitoring pages.

Web: Blocklists gets Add/Edit/Remove for BlockList objects (and shows ones the
resolver has not compiled yet); Clients marks cluster groups and locks config-file
groups and the default policy; Upstreams becomes a read-only health view when the
config cannot be written; Zones and Domains lose the managed-directory and list
copy; the Local zones page and its API calls are removed; nav is Status,
Blocklists, Domains, Lookup, Address redirects, Zones, Clients, Upstreams, Settings.

Console: a change to a group the resolver's config file defines is refused with the
reason instead of being skipped silently, and /api/clients says what the console
can and cannot edit.

Server: isFileWritable called a read-only existing file (a mounted ConfigMap)
writable, and api.allow_config_write was not reflected in the writable flag nor
checked by the upstream/ACL edit path. Both fixed; tests pin them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
matsandClaude Sonnet 5.5 committed 2026-10-16 10:56:21 +02:00
1 parent 7d58b80583
commit 7fa48bcdf9
18 files changed
+726 -404

No files matched your search

+12 -2
View File
@@ -48,7 +48,7 @@ func (s *Server) handleStatus(w http.ResponseWriter, _ *http.Request) {
"generation": s.hooks.Store.Generation(),
"path": s.hooks.Store.Path(),
"last_error": s.hooks.Store.LastError(),
"writable": isFileWritable(s.hooks.Store.Path()),
"writable": s.configWritable(),
"reload_ready": s.hooks.Store.Path() != "",
},
"components": s.hooks.Status(),
@@ -460,7 +460,7 @@ func (s *Server) handleSettingsGet(w http.ResponseWriter, _ *http.Request) {
"defaults_yaml": string(defaults),
"last_error": s.hooks.Store.LastError(),
"reload_available": path != "",
"writable": isFileWritable(path),
"writable": s.configWritable(),
})
}
@@ -555,12 +555,22 @@ func (s *Server) handleSettingsReload(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "version": s.hooks.Store.Get().Version})
}
// configWritable is whether an edit to the config can land: the operator allows
// it (api.allow_config_write) and the file accepts a write. The console uses it
// to decide between an editor and a read-only view.
func (s *Server) configWritable() bool {
return s.hooks.Store.Get().API.AllowConfigWrite && isFileWritable(s.hooks.Store.Path())
}
// commit writes the config to disk, swaps it in and notifies the cluster.
func (s *Server) commit(next *config.Config) error {
path := s.hooks.Store.Path()
if path == "" {
return fmt.Errorf("cannot persist: no config file path is known")
}
if !s.hooks.Store.Get().API.AllowConfigWrite {
return fmt.Errorf("cannot persist: config writes are disabled (api.allow_config_write is false)")
}
if !isFileWritable(path) {
return fmt.Errorf("cannot persist: %s is not writable", path)
}