Modular, context-aware and aspect-oriented dendritic Nix configurations. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ den.oeiuwq.com
configurations den dendritic nix aspect oriented

Dedup includes on ctxApply (#185)

Fixes #180

authored by oeiuwq.com and committed by

GitHub 0c108ece b839da13

+256 -105
+90 -92
modules/context/types.nix
··· 1 - # den.ctx — Declarative context definitions. 2 - # 3 - # A context is an attribute set whose attrNames (not values) determine 4 - # which parametric functions match. den.ctx provides named context 5 - # types with declarative transformations and aspect lookup. 6 - # 7 - # Named contexts carry semantic meaning beyond their structure. 8 - # ctx.host { host } and ctx.hm-host { host } hold the same data, 9 - # but hm-host guarantees home-manager support was validated — 10 - # following transform-don't-validate: you cannot obtain an hm-host 11 - # unless all detection criteria passed. 12 - # 13 - # Context types are independent of NixOS. Den can be used as a library 14 - # for any domain configurable through Nix (cloud infra, containers, 15 - # helm charts, etc). The OS framework is one implementation on top of 16 - # this context+aspects library. 17 - # 18 - # Shape of a context definition: 19 - # 20 - # den.ctx.foobar = { 21 - # desc = "The {foo,bar} context"; 22 - # conf = { foo, bar }: den.aspects.${foo}._.${bar}; 23 - # includes = [ <parametric aspects for this context> ]; 24 - # into = { 25 - # baz = { foo, bar }: lib.singleton { baz = 22; }; 26 - # }; 27 - # }; 28 - # 29 - # A context type is callable (it is a functor): 30 - # 31 - # aspect = den.ctx.foobar { foo = "hello"; bar = "world"; }; 32 - # 33 - # ctxApply produces an aspect that includes: owned configs from the 34 - # context type itself, the located aspect via conf, and all recursive 35 - # transformations via into. 36 - # 37 - # Transformations have type: source -> [ target ]. This allows fan-out 38 - # (one host producing many { host, user } pairs via map) and conditional 39 - # propagation (lib.optional for detection gates like hm-host). 40 - # 41 - # den.default is an alias for den.ctx.default. Every context type 42 - # transforms into default, so den.default.includes runs at every 43 - # pipeline stage. Use take.exactly to restrict matching. 44 - # 45 - # See os.nix, defaults.nix and provides/home-manager/ for built-in 46 - # context types. 47 { den, lib, ... }: 48 let 49 - inherit (den.lib) parametric take; 50 inherit (den.lib.aspects.types) providerType; 51 52 - ctxType = lib.types.submodule { 53 - freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; 54 - options = { 55 - desc = lib.mkOption { 56 - description = "Context description"; 57 - type = lib.types.str; 58 - default = ""; 59 - }; 60 - conf = lib.mkOption { 61 - description = "Obtain a configuration aspect for context"; 62 - type = lib.types.functionTo providerType; 63 - default = { }; 64 - }; 65 - into = lib.mkOption { 66 - description = "Context transformations"; 67 - type = lib.types.lazyAttrsOf (lib.types.functionTo (lib.types.listOf lib.types.raw)); 68 - default = { }; 69 - }; 70 - includes = lib.mkOption { 71 - description = "List of parametric aspects to include for this context"; 72 - type = lib.types.listOf providerType; 73 - default = [ ]; 74 - }; 75 - __functor = lib.mkOption { 76 - description = "Apply context. Returns a aspect with all dependencies."; 77 - type = lib.types.functionTo (lib.types.functionTo providerType); 78 - readOnly = true; 79 - internal = true; 80 - visible = false; 81 - default = ctxApply; 82 }; 83 - }; 84 - }; 85 86 cleanCtx = 87 ctx: 88 builtins.removeAttrs ctx [ 89 "desc" 90 "conf" 91 "into" 92 "__functor" 93 ]; 94 95 - # Given a context, returns an aspect that also includes 96 - # the result of all context propagation. 97 - ctxApply = 98 self: ctx: 99 let 100 - myself = parametric.fixedTo ctx (cleanCtx self); 101 - located = self.conf ctx; 102 - adapted = lib.mapAttrsToList (name: into: map den.ctx.${name} (into ctx)) self.into; 103 in 104 - { 105 - includes = lib.flatten [ 106 - myself 107 - located 108 - adapted 109 - ]; 110 - }; 111 112 in 113 {
··· 1 { den, lib, ... }: 2 let 3 + inherit (den.lib) parametric; 4 inherit (den.lib.aspects.types) providerType; 5 6 + ctxType = lib.types.submodule ( 7 + { name, ... }: 8 + { 9 + freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; 10 + options = { 11 + name = lib.mkOption { 12 + description = "Context type name"; 13 + type = lib.types.str; 14 + default = name; 15 + }; 16 + desc = lib.mkOption { 17 + description = "Context description"; 18 + type = lib.types.str; 19 + default = ""; 20 + }; 21 + conf = lib.mkOption { 22 + description = "Obtain a configuration aspect for context"; 23 + type = lib.types.functionTo providerType; 24 + default = { }; 25 + }; 26 + into = lib.mkOption { 27 + description = "Context transformations"; 28 + type = lib.types.lazyAttrsOf (lib.types.functionTo (lib.types.listOf lib.types.raw)); 29 + default = { }; 30 + }; 31 + includes = lib.mkOption { 32 + description = "Parametric aspects to include for this context"; 33 + type = lib.types.listOf providerType; 34 + default = [ ]; 35 + }; 36 + __functor = lib.mkOption { 37 + description = "Apply context with dedup across into targets."; 38 + type = lib.types.functionTo (lib.types.functionTo providerType); 39 + readOnly = true; 40 + internal = true; 41 + visible = false; 42 + default = ctxApply; 43 + }; 44 }; 45 + } 46 + ); 47 48 cleanCtx = 49 ctx: 50 builtins.removeAttrs ctx [ 51 + "name" 52 "desc" 53 "conf" 54 "into" 55 "__functor" 56 ]; 57 58 + collectPairs = 59 self: ctx: 60 + [ 61 + { 62 + inherit ctx; 63 + ctxDef = self; 64 + } 65 + ] 66 + ++ lib.concatLists ( 67 + lib.mapAttrsToList (n: into: lib.concatMap (v: collectPairs den.ctx.${n} v) (into ctx)) self.into 68 + ); 69 + 70 + dedupIncludes = 71 let 72 + go = 73 + acc: remaining: 74 + if remaining == [ ] then 75 + acc.result 76 + else 77 + let 78 + p = builtins.head remaining; 79 + rest = builtins.tail remaining; 80 + n = p.ctxDef.name; 81 + clean = cleanCtx p.ctxDef; 82 + isFirst = !(acc.seen ? ${n}); 83 + items = 84 + if isFirst then 85 + [ 86 + (parametric.fixedTo p.ctx clean) 87 + (p.ctxDef.conf p.ctx) 88 + ] 89 + else 90 + [ 91 + (parametric.atLeast clean p.ctx) 92 + (p.ctxDef.conf p.ctx) 93 + ]; 94 + in 95 + go { 96 + seen = acc.seen // { 97 + ${n} = true; 98 + }; 99 + result = acc.result ++ items; 100 + } rest; 101 in 102 + pairs: 103 + go { 104 + seen = { }; 105 + result = [ ]; 106 + } pairs; 107 + 108 + ctxApply = self: ctx: { includes = dedupIncludes (collectPairs self ctx); }; 109 110 in 111 {
-13
templates/ci/modules/features/context/host-propagation.nix
··· 4 # This test uses the `funny.names` test option to 5 # demostrate different places and context-aspects that 6 # can contribute configurations to the host. 7 - # 8 - # Note that both host and user aspects include default 9 - # and because of that, default owned and static values 10 - # might be duplicated. This is why users are NOT adviced 11 - # to abuse den.default. 12 - # 13 - # The behaviour is correct, but using den.default for 14 - # everything without care will cause deplication problems. 15 - # Instead, users should include either directly on the host 16 - # or by using `den.ctx.host` or `den.ctx.user`. 17 flake.tests.ctx-transformation.test-host = denTest ( 18 { 19 den, ··· 161 162 expected = [ 163 "default-anyctx {aspect-chain,class}" 164 - "default-anyctx {aspect-chain,class}" 165 "default-anyctx {host,user}" 166 "default-anyctx {host}" 167 ··· 170 "default-host-lax {host}" 171 172 "default-owned" 173 - "default-owned" 174 175 - "default-static" 176 "default-static" 177 178 "default-user-lax {host,user}"
··· 4 # This test uses the `funny.names` test option to 5 # demostrate different places and context-aspects that 6 # can contribute configurations to the host. 7 flake.tests.ctx-transformation.test-host = denTest ( 8 { 9 den, ··· 151 152 expected = [ 153 "default-anyctx {aspect-chain,class}" 154 "default-anyctx {host,user}" 155 "default-anyctx {host}" 156 ··· 159 "default-host-lax {host}" 160 161 "default-owned" 162 163 "default-static" 164 165 "default-user-lax {host,user}"
+145
templates/ci/modules/features/deadbugs/static-include-dup-package.nix
··· 86 } 87 ); 88 89 }
··· 86 } 87 ); 88 89 + flake.tests.deadbugs.dups.test-default-owned-package = denTest ( 90 + { 91 + den, 92 + lib, 93 + igloo, 94 + ... 95 + }: 96 + { 97 + den.hosts.x86_64-linux.igloo.users.tux = { }; 98 + 99 + den.default.nixos = 100 + { pkgs, ... }: 101 + { 102 + services.locate.package = pkgs.plocate; 103 + }; 104 + 105 + expr = lib.getName igloo.services.locate.package; 106 + expected = "plocate"; 107 + } 108 + ); 109 + 110 + flake.tests.deadbugs.dups.test-default-static-package = denTest ( 111 + { 112 + den, 113 + lib, 114 + igloo, 115 + ... 116 + }: 117 + { 118 + den.hosts.x86_64-linux.igloo.users.tux = { }; 119 + 120 + den.default.includes = [ 121 + { 122 + nixos = 123 + { pkgs, ... }: 124 + { 125 + services.locate.package = pkgs.plocate; 126 + }; 127 + } 128 + ]; 129 + 130 + expr = lib.getName igloo.services.locate.package; 131 + expected = "plocate"; 132 + } 133 + ); 134 + 135 + flake.tests.deadbugs.dups.test-default-owned-list = denTest ( 136 + { 137 + den, 138 + lib, 139 + igloo, 140 + ... 141 + }: 142 + { 143 + den.hosts.x86_64-linux.igloo.users.tux = { }; 144 + 145 + den.default.nixos.imports = [ 146 + { 147 + options.tags = lib.mkOption { 148 + type = lib.types.listOf lib.types.str; 149 + default = [ ]; 150 + }; 151 + } 152 + ]; 153 + den.default.nixos.tags = [ "server" ]; 154 + 155 + expr = igloo.tags; 156 + expected = [ "server" ]; 157 + } 158 + ); 159 + 160 + flake.tests.deadbugs.dups.test-host-owned-package = denTest ( 161 + { 162 + den, 163 + lib, 164 + igloo, 165 + ... 166 + }: 167 + { 168 + den.hosts.x86_64-linux.igloo.users.tux = { }; 169 + 170 + den.aspects.igloo.nixos = 171 + { pkgs, ... }: 172 + { 173 + services.locate.package = pkgs.plocate; 174 + }; 175 + 176 + expr = lib.getName igloo.services.locate.package; 177 + expected = "plocate"; 178 + } 179 + ); 180 + 181 + flake.tests.deadbugs.dups.test-host-owned-list = denTest ( 182 + { 183 + den, 184 + lib, 185 + igloo, 186 + ... 187 + }: 188 + { 189 + den.hosts.x86_64-linux.igloo.users.tux = { }; 190 + 191 + den.aspects.igloo.nixos.imports = [ 192 + { 193 + options.tags = lib.mkOption { 194 + type = lib.types.listOf lib.types.str; 195 + default = [ ]; 196 + }; 197 + } 198 + ]; 199 + den.aspects.igloo.nixos.tags = [ "server" ]; 200 + 201 + expr = igloo.tags; 202 + expected = [ "server" ]; 203 + } 204 + ); 205 + 206 + flake.tests.deadbugs.dups.test-default-list-multi-user = denTest ( 207 + { 208 + den, 209 + lib, 210 + igloo, 211 + ... 212 + }: 213 + { 214 + den.hosts.x86_64-linux.igloo.users = { 215 + tux = { }; 216 + pingu = { }; 217 + }; 218 + 219 + den.default.nixos.imports = [ 220 + { 221 + options.tags = lib.mkOption { 222 + type = lib.types.listOf lib.types.str; 223 + default = [ ]; 224 + }; 225 + } 226 + ]; 227 + den.default.nixos.tags = [ "server" ]; 228 + 229 + expr = igloo.tags; 230 + expected = [ "server" ]; 231 + } 232 + ); 233 + 234 }
+21
templates/ci/modules/features/default-includes.nix
··· 2 { 3 flake.tests.default-includes = { 4 5 test-set-hostname-from-host-context = denTest ( 6 { den, igloo, ... }: 7 {
··· 2 { 3 flake.tests.default-includes = { 4 5 + test-setting-host-service-package = denTest ( 6 + { 7 + den, 8 + lib, 9 + igloo, 10 + ... 11 + }: 12 + { 13 + den.hosts.x86_64-linux.igloo.users.tux = { }; 14 + 15 + den.aspects.igloo.nixos = 16 + { pkgs, ... }: 17 + { 18 + services.locate.package = pkgs.plocate; 19 + }; 20 + 21 + expr = lib.getName igloo.services.locate.package; 22 + expected = "plocate"; 23 + } 24 + ); 25 + 26 test-set-hostname-from-host-context = denTest ( 27 { den, igloo, ... }: 28 {