Files
mats 66f36cfac6
build / build-push (push) Successful in 1m11s
more stuff
2026-10-16 14:49:50 +02:00

141 lines
3.9 KiB
Go

package kubeedit
import (
"context"
"fmt"
"sort"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/kubesource"
)
// GroupObject is a stored ClientGroup.
type GroupObject struct {
Name string
Spec kubesource.ClientGroupSpec
}
// Groups lists the ClientGroup objects in evaluation order.
func (e *Editor) Groups(ctx context.Context) ([]GroupObject, error) {
items, err := e.list(ctx, kubesource.GroupGVR)
if err != nil {
return nil, err
}
out := make([]GroupObject, 0, len(items))
for i := range items {
var s kubesource.ClientGroupSpec
if err := decodeSpec(&items[i], &s); err != nil {
continue // the runtime reports an undecodable object; the console skips it
}
out = append(out, GroupObject{Name: items[i].GetName(), Spec: s})
}
sort.SliceStable(out, func(i, j int) bool {
if out[i].Spec.Priority != out[j].Spec.Priority {
return out[i].Spec.Priority < out[j].Spec.Priority
}
return out[i].Name < out[j].Name
})
return out, nil
}
// SpecFromGroup converts the console's group to an object spec.
func SpecFromGroup(g config.Group, priority int32) kubesource.ClientGroupSpec {
return kubesource.ClientGroupSpec{
Priority: priority,
Comment: g.Comment,
Match: g.Match,
Policy: g.Policy,
AllowDomains: g.AllowDomains,
DenyDomains: g.DenyDomains,
SkipBlocklist: g.SkipBlocklist,
SkipCache: g.SkipCache,
RateLimit: g.RateLimit,
MaxConcurrent: g.MaxConcurrent,
LogQueries: g.LogQueries,
Upstreams: g.Upstreams,
}
}
// SyncResult says what SyncGroups changed.
type SyncResult struct {
Created, Updated, Deleted []string
}
// SyncGroups makes the stored ClientGroups match the console's list.
//
// desired is the full list the console sent, in the order the operator put it.
// Groups for which foreign returns true belong to something else (the runtime's
// YAML) and are neither written nor deleted. Every group is validated before
// anything is written, so a bad group changes nothing. Priorities follow the
// list position, so reordering in the console reorders evaluation.
func (e *Editor) SyncGroups(ctx context.Context, desired []config.Group, foreign func(string) bool, actor string) (SyncResult, error) {
var res SyncResult
existing, err := e.Groups(ctx)
if err != nil {
return res, err
}
have := make(map[string]GroupObject, len(existing))
for _, g := range existing {
have[g.Name] = g
}
type write struct {
name string
spec kubesource.ClientGroupSpec
new bool
}
var writes []write
keep := map[string]bool{}
for i, g := range desired {
if foreign != nil && foreign(g.Name) {
if _, ours := have[g.Name]; !ours {
continue
}
}
if err := checkName("group", g.Name); err != nil {
return res, err
}
spec := SpecFromGroup(g, int32(i+1)*10)
if _, err := kubesource.CheckGroup(g.Name, spec); err != nil {
return res, fmt.Errorf("group %q: %w", g.Name, err)
}
keep[g.Name] = true
cur, ok := have[g.Name]
switch {
case !ok:
writes = append(writes, write{g.Name, spec, true})
case !sameSpec(cur.Spec, spec):
writes = append(writes, write{g.Name, spec, false})
}
}
for _, w := range writes {
if _, err := e.upsert(ctx, kubesource.GroupGVR, "ClientGroup", w.name, actor, w.spec); err != nil {
return res, fmt.Errorf("write group %q: %w", w.name, err)
}
if w.new {
res.Created = append(res.Created, w.name)
} else {
res.Updated = append(res.Updated, w.name)
}
}
for _, g := range existing {
if keep[g.Name] {
continue
}
if err := e.remove(ctx, kubesource.GroupGVR, g.Name); err != nil {
return res, err
}
res.Deleted = append(res.Deleted, g.Name)
}
return res, nil
}
// CheckGroupName reports why name cannot be a ClientGroup's object name, so a
// validation endpoint can say it before a save does.
func CheckGroupName(name string) error {
if name == "" {
return fmt.Errorf("a group needs a name")
}
return checkName("group", name)
}