package kubesource import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "git.k8s-git-k6n.net/mats/fancy-dns/internal/config" ) func signature(s *Snapshot) string { b, _ := json.Marshal(struct { G []config.Group L []config.ListSource Z map[string]string O []Override B []string A []string }{s.Groups, s.Lists, s.Zones, s.Overrides, s.BlockNames, s.AllowNames}) sum := sha256.Sum256(b) return hex.EncodeToString(sum[:]) } // Merge returns base with the cluster's groups and lists added; base is not // modified. Precedence is deliberate: whatever the YAML declares is the // operator's and wins. YAML groups are evaluated before cluster groups, and a // cluster list that reuses a YAML list's name is dropped. The names of what was // dropped are returned so the caller can say so. func Merge(base *config.Config, snap *Snapshot) (*config.Config, []string) { if snap == nil { return base, nil } next := *base next.ACL = base.ACL next.ACL.Groups = append([]config.Group(nil), base.ACL.Groups...) next.Block = base.Block next.Block.Lists = append([]config.ListSource(nil), base.Block.Lists...) // Inline rules from DomainRule objects join the config's own. next.Block.BlockEntries = append(append([]string(nil), base.Block.BlockEntries...), snap.BlockNames...) next.Block.AllowEntries = append(append([]string(nil), base.Block.AllowEntries...), snap.AllowNames...) var dropped []string groups := map[string]bool{} for _, g := range base.ACL.Groups { groups[g.Name] = true } for _, g := range snap.Groups { if groups[g.Name] { dropped = append(dropped, "ClientGroup "+g.Name+" (name used in the YAML)") continue } next.ACL.Groups = append(next.ACL.Groups, g) } lists := map[string]bool{} for _, l := range base.Block.Lists { lists[l.Name] = true } for _, l := range snap.Lists { if lists[l.Name] { dropped = append(dropped, "BlockList "+l.Name+" (name used in the YAML)") continue } // A cluster object cannot name a file, so the runtime picks one: a copy // of the list under its own data directory. It is what lets a restart // start with the list when the network (or the cluster DNS, which may // be this very resolver) is not up yet. if filepath.Base(l.Name) == l.Name && l.Name != "." && l.Name != ".." { l.Path = filepath.Join("lists", l.Name) l.Path = filepath.Join(filepath.Dir(base.CachePath()), l.Path) } // The same defaults config.applyDefaults gives a YAML list. if l.Format == "" { l.Format = "auto" } if l.Refresh == 0 { l.Refresh = config.Duration(24 * time.Hour) } if l.Timeout <= 0 { l.Timeout = base.Server.Timeouts.List } if l.MaxBytes <= 0 { l.MaxBytes = 512 << 20 } next.Block.Lists = append(next.Block.Lists, l) } return &next, dropped } // SyncZoneDir makes dir hold exactly one .zone file per zone, written // atomically, and removes the files of zones that no longer exist. It reports // whether anything on disk changed. Files it did not write (anything that is // not *.zone) are left alone. func SyncZoneDir(dir string, zones map[string]string) (bool, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return false, fmt.Errorf("zone directory %s: %w", dir, err) } changed := false want := map[string]bool{} for apex, text := range zones { name := zoneFile(apex) want[name] = true path := filepath.Join(dir, name) if old, err := os.ReadFile(path); err == nil && string(old) == text { continue } tmp, err := os.CreateTemp(dir, ".kube-zone-*") if err != nil { return changed, err } _, werr := tmp.WriteString(text) cerr := tmp.Close() if werr != nil || cerr != nil { _ = os.Remove(tmp.Name()) return changed, fmt.Errorf("write %s: %w", name, firstErr(werr, cerr)) } if err := os.Chmod(tmp.Name(), 0o644); err != nil { _ = os.Remove(tmp.Name()) return changed, err } if err := os.Rename(tmp.Name(), path); err != nil { _ = os.Remove(tmp.Name()) return changed, err } changed = true } entries, err := os.ReadDir(dir) if err != nil { return changed, err } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".zone") || want[e.Name()] { continue } if err := os.Remove(filepath.Join(dir, e.Name())); err != nil { return changed, err } changed = true } return changed, nil } // zoneFile is the file name for an apex. The apex has been normalised by // NormalizeName, so it holds only DNS name characters; the wildcard label is // the one that is not file-safe on every platform. func zoneFile(apex string) string { n := strings.TrimSuffix(apex, ".") if n == "" { n = "root" } return strings.ReplaceAll(n, "*", "_wild") + ".zone" } func firstErr(errs ...error) error { for _, e := range errs { if e != nil { return e } } return nil } // Save writes the snapshot so a restart that cannot reach the apiserver still // has the last known policy. Problems are not saved. func Save(path string, s *Snapshot) error { b, err := json.Marshal(s) if err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } return config.WriteFileAtomic(path, b, 0o644) } // Load reads a snapshot written by Save. A missing file is (nil, nil). func Load(path string) (*Snapshot, error) { b, err := os.ReadFile(path) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } var s Snapshot if err := json.Unmarshal(b, &s); err != nil { return nil, fmt.Errorf("%s: %w", path, err) } if s.Zones == nil { s.Zones = map[string]string{} } return &s, nil }