greenhouse

kore

my public infrastructure monorepo

git clone [email protected]:kore.git

kore / nixos / features / kore.nix159 lines · 4699B · raw

1{ config, lib, kore, ... }:
2
3let
4  cfg = config.kore;
5  hostname = config.networking.hostName;
6  hasHome = lib.elem "home" cfg.currentHost.roles;
7  hostKey = name: ../../keys/hosts + "/${name}.pub";
8
9  identityOptions = { name, ... }:
10    let
11      dir = ../../keys/identities + "/${name}";
12      sshDir = dir + "/ssh";
13      gpgFile = dir + "/gpg.asc";
14
15      sshFiles = lib.optionals (builtins.pathExists sshDir)
16        (lib.filter (lib.hasSuffix ".pub") (lib.attrNames (builtins.readDir sshDir)));
17    in {
18      options = with lib; {
19        email = mkOption {
20          type = types.nullOr types.str;
21          default = null;
22        };
23        gpg = mkOption {
24          type = types.nullOr types.str;
25          default = null;
26        };
27        sshKeys = mkOption {
28          type = types.listOf types.str;
29          default = map (file: lib.fileContents (sshDir + "/${file}")) sshFiles;
30        };
31        gpgKey = mkOption {
32          type = types.nullOr types.path;
33          default = if builtins.pathExists gpgFile then gpgFile else null;
34        };
35      };
36    };
37
38  hostOptions = with lib; {
39    system = mkOption {
40      type = types.str;
41    };
42    stateVersion = mkOption {
43      type = types.str;
44    };
45    roles = mkOption {
46      type = types.listOf types.str;
47      default = [];
48    };
49    users = mkOption {
50      type = types.listOf types.str;
51      default = [];
52    };
53    aliases = mkOption {
54      type = types.listOf types.str;
55      default = [];
56    };
57    ipv4 = mkOption {
58      type = types.nullOr types.str;
59      default = null;
60    };
61  };
62
63  repoOptions = with lib; {
64    write = mkOption {
65      type = types.listOf types.str;
66      default = [];
67    };
68    read = mkOption {
69      type = types.listOf types.str;
70      default = [];
71    };
72    description = mkOption {
73      type = types.str;
74      default = "";
75    };
76    owner = mkOption {
77      type = types.nullOr types.str;
78      default = null;
79    };
80  };
81
82  hostNames = name: host:
83    let names = [ name ] ++ host.aliases;
84    in map (n: "${n}.${config.networking.domain}") names ++ names;
85in {
86  options.kore = with lib; {
87    identities = mkOption {
88      type = types.attrsOf (types.submodule identityOptions);
89      default = {};
90    };
91    hosts = mkOption {
92      type = types.attrsOf (types.submodule [{ options = hostOptions; }]);
93      default = {};
94    };
95    currentHost = mkOption {
96      type = types.submodule [{ options = hostOptions; }];
97    };
98    repos = mkOption {
99      type = types.attrsOf (types.submodule [{ options = repoOptions; }]);
100      default = {};
101    };
102
103    impermanence.enable = mkOption {
104      type = types.bool;
105      default = true;
106    };
107  };
108
109  config = {
110    kore = {
111      identities = kore.identities or {};
112      hosts = kore.hosts;
113      currentHost = kore.hosts.${hostname};
114      repos = kore.repos or {};
115    };
116
117    assertions = map (user: {
118      assertion = cfg.identities ? ${user};
119      message = "kore: ${hostname} has user ${user}, but kore.json has no identity for it";
120    }) cfg.currentHost.users
121    ++ lib.concatLists (lib.mapAttrsToList (repo: acl: [{
122      assertion = builtins.match "[A-Za-z0-9_][A-Za-z0-9._-]*(/[A-Za-z0-9_][A-Za-z0-9._-]*)*" repo != null
123        && !(lib.hasSuffix ".git" repo);
124      message = "kore: repo name ${repo} is invalid";
125    }] ++ map (id: {
126      assertion = cfg.identities ? ${id};
127      message = "kore: repo ${repo} lists ${id}, but kore.json has no identity for it";
128    }) (acl.write ++ acl.read ++ lib.optional (acl.owner != null) acl.owner)) cfg.repos);
129
130    nixpkgs.hostPlatform = cfg.currentHost.system;
131    system.stateVersion = cfg.currentHost.stateVersion;
132
133    users.users = lib.listToAttrs (lib.imap0 (i: user: lib.nameValuePair user {
134      isNormalUser = true;
135      password = "tmp";
136      extraGroups = [ "wheel" ];
137
138      createHome = hasHome;
139      home = if hasHome then "/home/${user}" else "/var/empty";
140
141      subUidRanges = [{ startUid = 100000 + i * 65536; count = 65536; }];
142      subGidRanges = [{ startGid = 100000 + i * 65536; count = 65536; }];
143
144      openssh.authorizedKeys.keys = cfg.identities.${user}.sshKeys or [];
145    }) cfg.currentHost.users);
146
147    # /etc/hosts so machines resolve each other by name.
148    networking.hosts = lib.mkMerge (lib.mapAttrsToList (name: host:
149      lib.optionalAttrs (host.ipv4 != null) {
150        "${host.ipv4}" = hostNames name host;
151      }) cfg.hosts);
152
153    programs.ssh.knownHosts = lib.mapAttrs (name: host: {
154      hostNames = hostNames name host
155        ++ lib.optional (host.ipv4 != null) host.ipv4;
156      publicKeyFile = hostKey name;
157    }) (lib.filterAttrs (name: _: builtins.pathExists (hostKey name)) cfg.hosts);
158  };
159}