greenhouse

kore

my public infrastructure monorepo

git clone [email protected]:kore.git

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