greenhouse

kore

my public infrastructure monorepo

git clone [email protected]:kore.git

kore / nixos / features / kore.nix151 lines · 4484B · 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  };
73
74  hostNames = name: host:
75    let names = [ name ] ++ host.aliases;
76    in map (n: "${n}.${config.networking.domain}") names ++ names;
77in {
78  options.kore = with lib; {
79    identities = mkOption {
80      type = types.attrsOf (types.submodule identityOptions);
81      default = {};
82    };
83    hosts = mkOption {
84      type = types.attrsOf (types.submodule [{ options = hostOptions; }]);
85      default = {};
86    };
87    currentHost = mkOption {
88      type = types.submodule [{ options = hostOptions; }];
89    };
90    repos = mkOption {
91      type = types.attrsOf (types.submodule [{ options = repoOptions; }]);
92      default = {};
93    };
94
95    impermanence.enable = mkOption {
96      type = types.bool;
97      default = true;
98    };
99  };
100
101  config = {
102    kore = {
103      identities = kore.identities or {};
104      hosts = kore.hosts;
105      currentHost = kore.hosts.${hostname};
106      repos = kore.repos or {};
107    };
108
109    assertions = map (user: {
110      assertion = cfg.identities ? ${user};
111      message = "kore: ${hostname} has user ${user}, but kore.json has no identity for it";
112    }) cfg.currentHost.users
113    ++ lib.concatLists (lib.mapAttrsToList (repo: acl: [{
114      assertion = builtins.match "[A-Za-z0-9_][A-Za-z0-9._-]*(/[A-Za-z0-9_][A-Za-z0-9._-]*)*" repo != null
115        && !(lib.hasSuffix ".git" repo);
116      message = "kore: repo name ${repo} is invalid";
117    }] ++ map (id: {
118      assertion = cfg.identities ? ${id};
119      message = "kore: repo ${repo} lists ${id}, but kore.json has no identity for it";
120    }) (acl.write ++ acl.read)) cfg.repos);
121
122    nixpkgs.hostPlatform = cfg.currentHost.system;
123    system.stateVersion = cfg.currentHost.stateVersion;
124
125    users.users = lib.listToAttrs (lib.imap0 (i: user: lib.nameValuePair user {
126      isNormalUser = true;
127      password = "tmp";
128      extraGroups = [ "wheel" ];
129
130      createHome = hasHome;
131      home = if hasHome then "/home/${user}" else "/var/empty";
132
133      subUidRanges = [{ startUid = 100000 + i * 65536; count = 65536; }];
134      subGidRanges = [{ startGid = 100000 + i * 65536; count = 65536; }];
135
136      openssh.authorizedKeys.keys = cfg.identities.${user}.sshKeys or [];
137    }) cfg.currentHost.users);
138
139    # /etc/hosts so machines resolve each other by name.
140    networking.hosts = lib.mkMerge (lib.mapAttrsToList (name: host:
141      lib.optionalAttrs (host.ipv4 != null) {
142        "${host.ipv4}" = hostNames name host;
143      }) cfg.hosts);
144
145    programs.ssh.knownHosts = lib.mapAttrs (name: host: {
146      hostNames = hostNames name host
147        ++ lib.optional (host.ipv4 != null) host.ipv4;
148      publicKeyFile = hostKey name;
149    }) (lib.filterAttrs (name: _: builtins.pathExists (hostKey name)) cfg.hosts);
150  };
151}