Merge pull request #81460 from vcunat/p/knot-nixConfig

nixos/knot: allow full configuration by nix values

authored by

Martin Weinelt and committed by
GitHub
1ff350f7 ffa0af29

+207 -92
+2
nixos/doc/manual/release-notes/rl-2311.section.md
··· 98 98 99 99 - `pass` now does not contain `password-store.el`. Users should get `password-store.el` from Emacs lisp package set `emacs.pkgs.password-store`. 100 100 101 + - `services.knot` now supports `.settings` from RFC42. The change is not 100% compatible with the previous `.extraConfig`. 102 + 101 103 - `mu` now does not install `mu4e` files by default. Users should get `mu4e` from Emacs lisp package set `emacs.pkgs.mu4e`. 102 104 103 105 - `mariadb` now defaults to `mariadb_1011` instead of `mariadb_106`, meaning the default version was upgraded from 10.6.x to 10.11.x. See the [upgrade notes](https://mariadb.com/kb/en/upgrading-from-mariadb-10-6-to-mariadb-10-11/) for potential issues.
+125 -7
nixos/modules/services/networking/knot.nix
··· 5 5 let 6 6 cfg = config.services.knot; 7 7 8 - configFile = pkgs.writeTextFile { 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 + 105 + configFile = if cfg.settingsFile != null then 106 + assert cfg.settings == {} && cfg.keyFiles == []; 107 + cfg.settingsFile 108 + else pkgs.writeTextFile { 9 109 name = "knot.conf"; 10 - text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + 11 - cfg.extraConfig; 110 + text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + yamlConfig; 111 + # TODO: maybe we could do some checks even when private keys complicate this? 12 112 checkPhase = lib.optionalString (cfg.keyFiles == []) '' 13 113 ${cfg.package}/bin/knotc --config=$out conf-check 14 114 ''; ··· 60 160 ''; 61 161 }; 62 162 63 - extraConfig = mkOption { 64 - type = types.lines; 65 - default = ""; 163 + settings = mkOption { 164 + type = types.attrs; 165 + default = {}; 66 166 description = lib.mdDoc '' 67 - Extra lines to be added verbatim to knot.conf 167 + Extra configuration as nix values. 168 + ''; 169 + }; 170 + 171 + settingsFile = mkOption { 172 + type = types.nullOr types.path; 173 + default = null; 174 + description = lib.mdDoc '' 175 + As alternative to ``settings``, you can provide whole configuration 176 + directly in the almost-YAML format of Knot DNS. 177 + You might want to utilize ``writeTextFile`` for this. 68 178 ''; 69 179 }; 70 180 ··· 78 188 }; 79 189 }; 80 190 }; 191 + imports = [ 192 + # Compatibility with NixOS 23.05. At least partial, as it fails assert if used with keyFiles. 193 + (mkChangedOptionModule [ "services" "knot" "extraConfig" ] [ "services" "knot" "settingsFile" ] 194 + (config: pkgs.writeText "knot.conf" config.services.knot.extraConfig) 195 + ) 196 + ]; 81 197 82 198 config = mkIf config.services.knot.enable { 83 199 users.groups.knot = {}; ··· 86 202 group = "knot"; 87 203 description = "Knot daemon user"; 88 204 }; 205 + 206 + environment.etc."knot/knot.conf".source = configFile; # just for user's convenience 89 207 90 208 systemd.services.knot = { 91 209 unitConfig.Documentation = "man:knotd(8) man:knot.conf(5) man:knotc(8) https://www.knot-dns.cz/docs/${cfg.package.version}/html/";
+22 -21
nixos/tests/kea.nix
··· 134 134 extraArgs = [ 135 135 "-v" 136 136 ]; 137 - extraConfig = '' 138 - server: 139 - listen: 0.0.0.0@53 137 + settings = { 138 + server.listen = [ 139 + "0.0.0.0@53" 140 + ]; 140 141 141 - log: 142 - - target: syslog 143 - any: debug 142 + log.syslog.any = "info"; 144 143 145 - acl: 146 - - id: dhcp_ddns 147 - address: 10.0.0.1 148 - action: update 144 + acl.dhcp_ddns = { 145 + address = "10.0.0.1"; 146 + action = "update"; 147 + }; 149 148 150 - template: 151 - - id: default 152 - storage: ${zonesDir} 153 - zonefile-sync: -1 154 - zonefile-load: difference-no-serial 155 - journal-content: all 149 + template.default = { 150 + storage = zonesDir; 151 + zonefile-sync = "-1"; 152 + zonefile-load = "difference-no-serial"; 153 + journal-content = "all"; 154 + }; 156 155 157 - zone: 158 - - domain: lan.nixos.test 159 - file: lan.nixos.test.zone 160 - acl: [dhcp_ddns] 161 - ''; 156 + zone."lan.nixos.test" = { 157 + file = "lan.nixos.test.zone"; 158 + acl = [ 159 + "dhcp_ddns" 160 + ]; 161 + }; 162 + }; 162 163 }; 163 164 164 165 };
+58 -64
nixos/tests/knot.nix
··· 60 60 services.knot.enable = true; 61 61 services.knot.extraArgs = [ "-v" ]; 62 62 services.knot.keyFiles = [ tsigFile ]; 63 - services.knot.extraConfig = '' 64 - server: 65 - listen: 0.0.0.0@53 66 - listen: ::@53 67 - automatic-acl: true 63 + services.knot.settings = { 64 + server = { 65 + listen = [ 66 + "0.0.0.0@53" 67 + "::@53" 68 + ]; 69 + automatic-acl = true; 70 + }; 68 71 69 - remote: 70 - - id: secondary 71 - address: 192.168.0.2@53 72 - key: xfr_key 72 + acl.secondary_acl = { 73 + address = "192.168.0.2"; 74 + key = "xfr_key"; 75 + action = "transfer"; 76 + }; 73 77 74 - template: 75 - - id: default 76 - storage: ${knotZonesEnv} 77 - notify: [secondary] 78 - dnssec-signing: on 79 - # Input-only zone files 80 - # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3 81 - # prevents modification of the zonefiles, since the zonefiles are immutable 82 - zonefile-sync: -1 83 - zonefile-load: difference 84 - journal-content: changes 85 - # move databases below the state directory, because they need to be writable 86 - journal-db: /var/lib/knot/journal 87 - kasp-db: /var/lib/knot/kasp 88 - timer-db: /var/lib/knot/timer 78 + remote.secondary.address = "192.168.0.2@53"; 89 79 90 - zone: 91 - - domain: example.com 92 - file: example.com.zone 80 + template.default = { 81 + storage = knotZonesEnv; 82 + notify = [ "secondary" ]; 83 + acl = [ "secondary_acl" ]; 84 + dnssec-signing = true; 85 + # Input-only zone files 86 + # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3 87 + # prevents modification of the zonefiles, since the zonefiles are immutable 88 + zonefile-sync = -1; 89 + zonefile-load = "difference"; 90 + journal-content = "changes"; 91 + }; 93 92 94 - - domain: sub.example.com 95 - file: sub.example.com.zone 93 + zone = { 94 + "example.com".file = "example.com.zone"; 95 + "sub.example.com".file = "sub.example.com.zone"; 96 + }; 96 97 97 - log: 98 - - target: syslog 99 - any: info 100 - ''; 98 + log.syslog.any = "info"; 99 + }; 101 100 }; 102 101 103 102 secondary = { lib, ... }: { ··· 113 112 services.knot.enable = true; 114 113 services.knot.keyFiles = [ tsigFile ]; 115 114 services.knot.extraArgs = [ "-v" ]; 116 - services.knot.extraConfig = '' 117 - server: 118 - listen: 0.0.0.0@53 119 - listen: ::@53 120 - automatic-acl: true 115 + services.knot.settings = { 116 + server = { 117 + listen = [ 118 + "0.0.0.0@53" 119 + "::@53" 120 + ]; 121 + automatic-acl = true; 122 + }; 121 123 122 - remote: 123 - - id: primary 124 - address: 192.168.0.1@53 125 - key: xfr_key 124 + remote.primary = { 125 + address = "192.168.0.1@53"; 126 + key = "xfr_key"; 127 + }; 126 128 127 - template: 128 - - id: default 129 - master: primary 130 - # zonefileless setup 131 - # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2 132 - zonefile-sync: -1 133 - zonefile-load: none 134 - journal-content: all 135 - # move databases below the state directory, because they need to be writable 136 - journal-db: /var/lib/knot/journal 137 - kasp-db: /var/lib/knot/kasp 138 - timer-db: /var/lib/knot/timer 139 - 140 - zone: 141 - - domain: example.com 142 - file: example.com.zone 129 + template.default = { 130 + master = "primary"; 131 + # zonefileless setup 132 + # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2 133 + zonefile-sync = "-1"; 134 + zonefile-load = "none"; 135 + journal-content = "all"; 136 + }; 143 137 144 - - domain: sub.example.com 145 - file: sub.example.com.zone 138 + zone = { 139 + "example.com".file = "example.com.zone"; 140 + "sub.example.com".file = "sub.example.com.zone"; 141 + }; 146 142 147 - log: 148 - - target: syslog 149 - any: info 150 - ''; 143 + log.syslog.any = "info"; 144 + }; 151 145 }; 152 146 client = { lib, nodes, ... }: { 153 147 imports = [ common ];