Add a read-only Kubernetes source for zones, client groups and blocklists
The runtime can now take ClientGroup, BlockList and DNSZone objects (fancydns.k6n.net/v1alpha1) from a cluster with -kube, from a pod's service account or a kubeconfig alike. internal/kubesource watches them with dynamic informers, converts and validates each object on its own, and publishes an immutable snapshot. A bad object is skipped and keeps its last good version, so one broken edit cannot remove a running policy; an invalid merged config is dropped whole. The last snapshot is saved so a restart without apiserver access still serves. config.Store gains an overlay: Base() is what the YAML says, Get() is base plus cluster objects. Everything that writes the config back (mutateConfig, SetAdminAddr, the settings editor) now starts from Base(), otherwise cluster objects would be copied into the file and shadow their own objects. YAML wins every name collision. putACL and toggleBlocklist refuse to edit cluster-owned objects. BlockList has no path field and refuses file:// URLs so a namespaced object cannot pick files the runtime reads or writes. DNSZone is rendered through the zone writer's own prove-by-reparse step, with the SOA serial taken from metadata.generation so every replica renders identical bytes. qlog: Flush reset the pending slice to [:0], so entries logged while it was still storing the batch were appended over the same backing array. The race detector found it through the new Kubernetes integration test. pending is now set to nil, with a regression test that fails without the change. CRD manifests, RBAC and examples are in deploy/k8s/crds; tests pin the schemas to the Go spec structs so the two cannot drift. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
340b4eaf80
commit
4d38945d94
27 files changed
+2250
-19
No files matched your search
+30
-1
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -431,6 +432,9 @@ func (a *App) toggleBlocklist(name string, enabled bool) error {
|
||||
if a.blocks == nil {
|
||||
return fmt.Errorf("blocklists are disabled")
|
||||
}
|
||||
if a.clusterOwned("list", name) {
|
||||
return fmt.Errorf("blocklist %q is a Kubernetes BlockList; set spec.disabled on the object instead", name)
|
||||
}
|
||||
err := a.mutateConfig(func(c *config.Config) error {
|
||||
found := false
|
||||
for i := range c.Block.Lists {
|
||||
@@ -459,7 +463,10 @@ func (a *App) toggleBlocklist(name string, enabled bool) error {
|
||||
// mutateConfig clones, mutates, validates and commits the config, returning
|
||||
// the validation error when the change is rejected.
|
||||
func (a *App) mutateConfig(fn func(*config.Config) error) error {
|
||||
cur := a.cfg.Get()
|
||||
// Start from the base, not the effective config: the effective one holds
|
||||
// the cluster's groups and lists, and persisting those would copy them
|
||||
// into the YAML file where they would shadow their own objects.
|
||||
cur := a.cfg.Base()
|
||||
next := *cur
|
||||
// A shallow copy is not enough: the slices and maps we touch need their own
|
||||
// backing arrays, or a rejected change would corrupt the live config.
|
||||
@@ -767,6 +774,28 @@ func (a *App) getACL() any {
|
||||
|
||||
// putACL validates and persists new ACL groups.
|
||||
func (a *App) putACL(_ context.Context, groups []config.Group, def string) (int64, error) {
|
||||
// The console shows the effective list, so a save sends the cluster's
|
||||
// groups back along with the file's. Unchanged ones are dropped here (the
|
||||
// cluster still owns them); a changed one is refused, because the write
|
||||
// would land in the YAML and silently shadow the object.
|
||||
if snap := a.kubeSnap.Load(); snap != nil {
|
||||
owned := make(map[string]config.Group, len(snap.Groups))
|
||||
for _, g := range snap.Groups {
|
||||
owned[g.Name] = g
|
||||
}
|
||||
kept := groups[:0:0]
|
||||
for _, g := range groups {
|
||||
cg, ok := owned[g.Name]
|
||||
if !ok {
|
||||
kept = append(kept, g)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(g, cg) {
|
||||
return 0, fmt.Errorf("group %q is a Kubernetes ClientGroup; edit the object instead", g.Name)
|
||||
}
|
||||
}
|
||||
groups = kept
|
||||
}
|
||||
err := a.mutateConfig(func(c *config.Config) error {
|
||||
c.ACL.Groups = groups
|
||||
if def != "" {
|
||||
|
||||
Reference in new issue
Block a user