package kubeedit import ( "context" "fmt" "slices" "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/retry" "git.k8s-git-k6n.net/mats/fancy-dns/internal/kubesource" ) // The console keeps the rules it adds in one DomainRule per action, so the // Domains page does not scatter an object per name. Objects an operator wrote // by hand are read, and a rule in them can be removed, but never added to. const ( inlineBlock = "inline-block" inlineAllow = "inline-allow" ) // AddRules adds names to the console's block or allow rule list and reports how // many it was given. Names already present are kept once. func (e *Editor) AddRules(ctx context.Context, action string, names []string, actor string) (int, error) { if _, err := kubesource.CheckRules(kubesource.DomainRuleSpec{Action: action, Names: names}); err != nil { return 0, err } canonical, _ := kubesource.CheckRules(kubesource.DomainRuleSpec{Action: action, Names: names}) obj := inlineBlock if action == "allow" { obj = inlineAllow } err := retry.RetryOnConflict(retry.DefaultRetry, func() error { cur, err := e.res(kubesource.RuleGVR).Get(ctx, obj, metav1.GetOptions{}) var spec kubesource.DomainRuleSpec switch { case apierrors.IsNotFound(err): spec = kubesource.DomainRuleSpec{Action: action, Comment: "Added from the console"} case err != nil: return err default: if err := decodeSpec(cur, &spec); err != nil { return err } } for _, n := range canonical.Names { if !slices.Contains(spec.Names, n) { spec.Names = append(spec.Names, n) } } // The whole list must still be one the runtime accepts. if _, err := kubesource.CheckRules(spec); err != nil { return err } _, err = e.upsert(ctx, kubesource.RuleGVR, "DomainRule", obj, actor, spec) return err }) return len(canonical.Names), err } // ruleKey is how a rule is compared: the runtime strips wildcards and the // trailing dot, so "ads.example.com", "*.ads.example.com." and // "**.ads.example.com" all name the same rule to delete. func ruleKey(n string) string { n = strings.ToLower(strings.TrimSpace(n)) n = strings.TrimPrefix(strings.TrimPrefix(n, "*."), "**.") return strings.TrimSuffix(n, ".") } // DeleteRule removes every inline rule that names target, in any DomainRule. func (e *Editor) DeleteRule(ctx context.Context, target, actor string) (int, error) { want := ruleKey(target) items, err := e.list(ctx, kubesource.RuleGVR) if err != nil { return 0, err } removed := 0 for i := range items { var spec kubesource.DomainRuleSpec if decodeSpec(&items[i], &spec) != nil { continue } keep := spec.Names[:0:0] for _, n := range spec.Names { if ruleKey(n) == want { removed++ continue } keep = append(keep, n) } if len(keep) == len(spec.Names) { continue } name := items[i].GetName() if len(keep) == 0 && (name == inlineBlock || name == inlineAllow) { if err := e.remove(ctx, kubesource.RuleGVR, name); err != nil { return removed, err } continue } spec.Names = keep if _, err := e.upsert(ctx, kubesource.RuleGVR, "DomainRule", name, actor, spec); err != nil { return removed, err } } if removed == 0 { return 0, fmt.Errorf("%w: no inline rule matches %q", ErrNotFound, target) } return removed, nil }