main
1{
2 lib,
3 den,
4 ...
5}: {
6 den.schema.host = {
7 options = let
8 # NOTE: Copied from https://github.com/sini/nix-config/blob/1a9f6a8f6ec5f6c324f9f4ce01630703dd6c1eaa/modules/den/schema/host.nix#L121
9 # TODO: This feature may be merge into den in furture. Check https://github.com/denful/den/pull/563
10 # Dynamic settings type — recursively discovers aspects that declare .settings.
11 # Mirrors the aspect tree: den.aspects.disk.zfs-disk-single.settings →
12 # host.settings.disk.zfs-disk-single.*
13 settingsType = let
14 inherit (den.lib.aspects.fx.keyClassification) structuralKeysSet;
15 classKeys = den.classes or {};
16 quirkKeys = den.quirks or {};
17 skipKey = k: k == "settings" || structuralKeysSet ? ${k} || classKeys ? ${k} || quirkKeys ? ${k};
18
19 # Settings declarations may be plain option attrsets
20 # (`{ foo = mkOption {...}; }`) or module-shaped with explicit
21 # imports/config. Default the module keys so plain attrsets work.
22 #
23 # imports'/config' are bound under distinct names on purpose: writing
24 # `imports = raw.imports or [ ]` here gets rewritten by statix (W04) to
25 # `inherit (raw) imports`, which DROPS the `or` default and throws when
26 # raw is a plain options attrset with no imports/config key.
27 reshapeSettings = raw: let
28 imports' = raw.imports or [];
29 config' = raw.config or {};
30 in {
31 imports = imports';
32 config = config';
33 options = removeAttrs raw [
34 "imports"
35 "config"
36 ];
37 };
38
39 # True if a node has .settings anywhere in its aspect subtree.
40 hasSettingsDeep = node:
41 builtins.isAttrs node
42 && (
43 (node ? settings)
44 || lib.any (k: !(skipKey k) && hasSettingsDeep (node.${k} or null)) (builtins.attrNames node)
45 );
46
47 # Build the submodule for one aspect-tree node, mirroring the tree.
48 # A node may be BOTH an aspect with .settings AND a parent of child
49 # aspects that have settings (e.g. services.bgp has localAsn settings and
50 # also parents services.bgp.cilium-bgp). Merge the node's own settings
51 # options with recursion into its settings-bearing children.
52 nodeModule = node: let
53 ownSettings =
54 if node ? settings
55 then reshapeSettings node.settings
56 else {
57 imports = [];
58 config = {};
59 options = {};
60 };
61 settingChildren =
62 lib.filterAttrs (
63 k: v: !(skipKey k) && builtins.isAttrs v && hasSettingsDeep v
64 )
65 node;
66 childOptions =
67 lib.mapAttrs (
68 name: child:
69 lib.mkOption {
70 type = lib.types.submodule (nodeModule child);
71 default = {};
72 description = "Settings under ${name}";
73 }
74 )
75 settingChildren;
76 # Distinct names so statix (W04) can't rewrite to
77 # `inherit (ownSettings) imports`, which would drop the `or` default.
78 ownImports = ownSettings.imports or [];
79 ownConfig = ownSettings.config or {};
80 in {
81 imports = ownImports;
82 config = ownConfig;
83 options = (ownSettings.options or {}) // childOptions;
84 };
85 in
86 lib.types.submodule (nodeModule (den.aspects or {}));
87 in {
88 settings = lib.mkOption {
89 description = "Per-aspect typed settings";
90 type = settingsType;
91 default = {};
92 };
93
94 pubKey = lib.mkOption {
95 description = "Public SSH key for the host.";
96 type = lib.types.nullOr lib.types.str;
97 default = null;
98 };
99
100 rootHashedPasswordFileAged = lib.mkOption {
101 description = "Age encrypted hashed password file path for host root user.";
102 type = lib.types.nullOr lib.types.path;
103 default = null;
104 };
105
106 address = {
107 ipv4 = {
108 clearText = lib.mkOption {
109 description = "Clear text of public IPv4 address for this host.";
110 type = lib.types.nullOr lib.types.str;
111 default = null;
112 };
113 secret = {
114 name = lib.mkOption {
115 description = "Secret name of public IPv4 address for this host.";
116 type = lib.types.nullOr lib.types.str;
117 default = null;
118 };
119 file = lib.mkOption {
120 description = "Secret file of public IPv4 address for this host.";
121 type = lib.types.nullOr lib.types.path;
122 default = null;
123 };
124 };
125 tailscale = lib.mkOption {
126 description = "Clear text of tailscale IPv4 address for this host.";
127 type = lib.types.nullOr lib.types.str;
128 default = null;
129 };
130 };
131 ipv6 = {
132 clearText = lib.mkOption {
133 description = "Clear text of public IPv6 address for this host.";
134 type = lib.types.nullOr lib.types.str;
135 default = null;
136 };
137 secret = {
138 name = lib.mkOption {
139 description = "Secret name of public IPv6 address for this host.";
140 type = lib.types.nullOr lib.types.str;
141 default = null;
142 };
143 file = lib.mkOption {
144 description = "Secret file of public IPv6 address for this host.";
145 type = lib.types.nullOr lib.types.path;
146 default = null;
147 };
148 };
149 tailscale = lib.mkOption {
150 description = "Clear text of tailscale IPv6 address for this host.";
151 type = lib.types.nullOr lib.types.str;
152 default = null;
153 };
154 };
155 };
156 };
157 };
158}