package main import ( "fmt" "os" "strings" "gopkg.in/yaml.v3" ) // CatalogConfig holds the configuration for a single catalog zone. type CatalogConfig struct { Zone string `yaml:"zone"` } // SOAConfig holds the configurable SOA fields. type SOAConfig struct { Mname string `yaml:"mname"` Rname string `yaml:"rname"` } // BindConfConfig holds BIND-specific configuration for domains.conf generation. type BindConfConfig struct { AlsoNotify map[string][]string `yaml:"also-notify"` } // Config holds the parsed catz.yaml configuration. type Config struct { Catalogs map[string]CatalogConfig `yaml:"catalogs"` SOA SOAConfig `yaml:"soa"` BindConf BindConfConfig `yaml:"bind-conf"` } func loadConfig(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading config: %w", err) } var cfg Config if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } if err := validateConfig(&cfg); err != nil { return nil, err } // Normalize zone FQDNs for name, cat := range cfg.Catalogs { cat.Zone = normalizeFQDN(cat.Zone) cfg.Catalogs[name] = cat } cfg.SOA.Mname = normalizeFQDN(cfg.SOA.Mname) cfg.SOA.Rname = normalizeFQDN(cfg.SOA.Rname) return &cfg, nil } func validateConfig(cfg *Config) error { if len(cfg.Catalogs) == 0 { return fmt.Errorf("config: no catalogs defined") } for name, cat := range cfg.Catalogs { if cat.Zone == "" { return fmt.Errorf("config: catalog %q has no zone defined", name) } } if cfg.SOA.Mname == "" { return fmt.Errorf("config: soa.mname is required") } if cfg.SOA.Rname == "" { return fmt.Errorf("config: soa.rname is required") } for catName := range cfg.BindConf.AlsoNotify { if _, ok := cfg.Catalogs[catName]; !ok { return fmt.Errorf("config: bind-conf.also-notify references unknown catalog %q", catName) } } return nil } func normalizeFQDN(name string) string { name = strings.ToLower(strings.TrimSpace(name)) if name != "" && !strings.HasSuffix(name, ".") { name += "." } return name }