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

+21 -12
View File
@@ -615,23 +615,32 @@ func queryTime(r *http.Request, key string) time.Time {
return time.Time{}
}
// isFileWritable reports whether the config file at path can be rewritten. A file
// that exists and refuses a write (a ConfigMap mounted into a pod, a read-only
// bind mount) is not writable: only a file that does not exist yet counts as
// writable, and then only when its directory accepts a new file.
func isFileWritable(path string) bool {
if path == "" {
return false
}
f, err := os.OpenFile(path, os.O_WRONLY, 0)
if err != nil {
// A missing file in a writable directory is still writable.
dir := path
if i := strings.LastIndexByte(dir, '/'); i > 0 {
dir = dir[:i]
}
d, derr := os.Stat(dir)
if derr != nil {
return false
}
return d.IsDir()
if err == nil {
_ = f.Close()
return true
}
_ = f.Close()
if !os.IsNotExist(err) {
return false
}
dir := "."
if i := strings.LastIndexByte(path, '/'); i > 0 {
dir = path[:i]
}
probe, err := os.CreateTemp(dir, ".fancy-dns-writable-*")
if err != nil {
return false
}
name := probe.Name()
_ = probe.Close()
_ = os.Remove(name)
return true
}