greenhouse

kore

my public infrastructure monorepo

git clone [email protected]:kore.git

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