lol

nixos/knot: allow full configuration by nix values (RFC 42)

+105 -6
+105 -6
nixos/modules/services/networking/knot.nix
··· 5 5 let 6 6 cfg = config.services.knot; 7 7 8 + yamlConfig = let 9 + result = assert secsCheck; nix2yaml cfg.settings; 10 + 11 + secAllow = n: hasPrefix "mod-" n || elem n [ 12 + "module" 13 + "server" "xdp" "control" 14 + "log" 15 + "statistics" "database" 16 + "keystore" "key" "remote" "remotes" "acl" "submission" "policy" 17 + "template" 18 + "zone" 19 + "include" 20 + ]; 21 + secsCheck = let 22 + secsBad = filter (n: !secAllow n) (attrNames cfg.settings); 23 + in if secsBad == [] then true else throw 24 + ("services.knot.settings contains unknown sections: " + toString secsBad); 25 + 26 + nix2yaml = nix_def: concatStrings ( 27 + # We output the config section in the upstream-mandated order. 28 + # Ordering is important due to forward-references not being allowed. 29 + # See definition of conf_export and 'const yp_item_t conf_schema' 30 + # upstream for reference. Last updated for 3.3. 31 + # When changing the set of sections, also update secAllow above. 32 + [ (sec_list_fa "id" nix_def "module") ] 33 + ++ map (sec_plain nix_def) 34 + [ "server" "xdp" "control" ] 35 + ++ [ (sec_list_fa "target" nix_def "log") ] 36 + ++ map (sec_plain nix_def) 37 + [ "statistics" "database" ] 38 + ++ map (sec_list_fa "id" nix_def) 39 + [ "keystore" "key" "remote" "remotes" "acl" "submission" "policy" ] 40 + 41 + # Export module sections before the template section. 42 + ++ map (sec_list_fa "id" nix_def) (filter (hasPrefix "mod-") (attrNames nix_def)) 43 + 44 + ++ [ (sec_list_fa "id" nix_def "template") ] 45 + ++ [ (sec_list_fa "domain" nix_def "zone") ] 46 + ++ [ (sec_plain nix_def "include") ] 47 + ); 48 + 49 + # A plain section contains directly attributes (we don't really check that ATM). 50 + sec_plain = nix_def: sec_name: if !hasAttr sec_name nix_def then "" else 51 + n2y "" { ${sec_name} = nix_def.${sec_name}; }; 52 + 53 + # This section contains a list of attribute sets. In each of the sets 54 + # there's an attribute (`fa_name`, typically "id") that must exist and come first. 55 + # Alternatively we support using attribute sets instead of lists; example diff: 56 + # -template = [ { id = "default"; /* other attributes */ } { id = "foo"; } ] 57 + # +template = { default = { /* those attributes */ }; foo = { }; } 58 + sec_list_fa = fa_name: nix_def: sec_name: if !hasAttr sec_name nix_def then "" else 59 + let 60 + elem2yaml = fa_val: other_attrs: 61 + " - " + n2y "" { ${fa_name} = fa_val; } 62 + + " " + n2y " " other_attrs 63 + + "\n"; 64 + sec = nix_def.${sec_name}; 65 + in 66 + sec_name + ":\n" + 67 + (if isList sec 68 + then flip concatMapStrings sec 69 + (elem: elem2yaml elem.${fa_name} (removeAttrs elem [ fa_name ])) 70 + else concatStrings (mapAttrsToList elem2yaml sec) 71 + ); 72 + 73 + # This convertor doesn't care about ordering of attributes. 74 + # TODO: it could probably be simplified even more, now that it's not 75 + # to be used directly, but we might want some other tweaks, too. 76 + n2y = indent: val: 77 + if doRecurse val then concatStringsSep "\n${indent}" 78 + (mapAttrsToList 79 + # This is a bit wacky - set directly under a set would start on bad indent, 80 + # so we start those on a new line, but not other types of attribute values. 81 + (aname: aval: "${aname}:${if doRecurse aval then "\n${indent} " else " "}" 82 + + n2y (indent + " ") aval) 83 + val 84 + ) 85 + + "\n" 86 + else 87 + /* 88 + if isList val && stringLength indent < 4 then concatMapStrings 89 + (elem: "\n${indent}- " + n2y (indent + " ") elem) 90 + val 91 + else 92 + */ 93 + if isList val /* and long indent */ then 94 + "[ " + concatMapStringsSep ", " quoteString val + " ]" else 95 + if isBool val then (if val then "on" else "off") else 96 + quoteString val; 97 + 98 + # We don't want paths like ./my-zone.txt be converted to plain strings. 99 + quoteString = s: ''"${if builtins.typeOf s == "path" then s else toString s}"''; 100 + # We don't want to walk the insides of derivation attributes. 101 + doRecurse = val: isAttrs val && !isDerivation val; 102 + 103 + in result; 104 + 8 105 configFile = pkgs.writeTextFile { 9 106 name = "knot.conf"; 10 - text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + 11 - cfg.extraConfig; 107 + text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + yamlConfig; 108 + # TODO: maybe we could do some checks even when private keys complicate this? 12 109 checkPhase = lib.optionalString (cfg.keyFiles == []) '' 13 110 ${cfg.package}/bin/knotc --config=$out conf-check 14 111 ''; ··· 60 157 ''; 61 158 }; 62 159 63 - extraConfig = mkOption { 64 - type = types.lines; 65 - default = ""; 160 + settings = mkOption { 161 + type = types.attrs; 162 + default = {}; 66 163 description = lib.mdDoc '' 67 - Extra lines to be added verbatim to knot.conf 164 + Extra configuration as nix values. 68 165 ''; 69 166 }; 70 167 ··· 86 183 group = "knot"; 87 184 description = "Knot daemon user"; 88 185 }; 186 + 187 + environment.etc."knot/knot.conf".source = configFile; # just for user's convenience 89 188 90 189 systemd.services.knot = { 91 190 unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/";