1{ config, lib, pkgs, ... }:
2with lib;
3let
4 cfg = config.services.nomad;
5 format = pkgs.formats.json { };
6in
7{
8 ##### interface
9 options = {
10 services.nomad = {
11 enable = mkEnableOption (lib.mdDoc "Nomad, a distributed, highly available, datacenter-aware scheduler");
12
13 package = mkOption {
14 type = types.package;
15 default = pkgs.nomad;
16 defaultText = literalExpression "pkgs.nomad";
17 description = lib.mdDoc ''
18 The package used for the Nomad agent and CLI.
19 '';
20 };
21
22 extraPackages = mkOption {
23 type = types.listOf types.package;
24 default = [ ];
25 description = lib.mdDoc ''
26 Extra packages to add to {env}`PATH` for the Nomad agent process.
27 '';
28 example = literalExpression ''
29 with pkgs; [ cni-plugins ]
30 '';
31 };
32
33 dropPrivileges = mkOption {
34 type = types.bool;
35 default = true;
36 description = lib.mdDoc ''
37 Whether the nomad agent should be run as a non-root nomad user.
38 '';
39 };
40
41 enableDocker = mkOption {
42 type = types.bool;
43 default = true;
44 description = lib.mdDoc ''
45 Enable Docker support. Needed for Nomad's docker driver.
46
47 Note that the docker group membership is effectively equivalent
48 to being root, see https://github.com/moby/moby/issues/9976.
49 '';
50 };
51
52 extraSettingsPaths = mkOption {
53 type = types.listOf types.path;
54 default = [ ];
55 description = lib.mdDoc ''
56 Additional settings paths used to configure nomad. These can be files or directories.
57 '';
58 example = literalExpression ''
59 [ "/etc/nomad-mutable.json" "/run/keys/nomad-with-secrets.json" "/etc/nomad/config.d" ]
60 '';
61 };
62
63 extraSettingsPlugins = mkOption {
64 type = types.listOf (types.either types.package types.path);
65 default = [ ];
66 description = lib.mdDoc ''
67 Additional plugins dir used to configure nomad.
68 '';
69 example = literalExpression ''
70 [ "<pluginDir>" "pkgs.<plugins-name>"]
71 '';
72 };
73
74
75 settings = mkOption {
76 type = format.type;
77 default = { };
78 description = lib.mdDoc ''
79 Configuration for Nomad. See the [documentation](https://www.nomadproject.io/docs/configuration)
80 for supported values.
81
82 Notes about `data_dir`:
83
84 If `data_dir` is set to a value other than the
85 default value of `"/var/lib/nomad"` it is the Nomad
86 cluster manager's responsibility to make sure that this directory
87 exists and has the appropriate permissions.
88
89 Additionally, if `dropPrivileges` is
90 `true` then `data_dir`
91 *cannot* be customized. Setting
92 `dropPrivileges` to `true` enables
93 the `DynamicUser` feature of systemd which directly
94 manages and operates on `StateDirectory`.
95 '';
96 example = literalExpression ''
97 {
98 # A minimal config example:
99 server = {
100 enabled = true;
101 bootstrap_expect = 1; # for demo; no fault tolerance
102 };
103 client = {
104 enabled = true;
105 };
106 }
107 '';
108 };
109 };
110 };
111
112 ##### implementation
113 config = mkIf cfg.enable {
114 services.nomad.settings = {
115 # Agrees with `StateDirectory = "nomad"` set below.
116 data_dir = mkDefault "/var/lib/nomad";
117 };
118
119 environment = {
120 etc."nomad.json".source = format.generate "nomad.json" cfg.settings;
121 systemPackages = [ cfg.package ];
122 };
123
124 systemd.services.nomad = {
125 description = "Nomad";
126 wantedBy = [ "multi-user.target" ];
127 wants = [ "network-online.target" ];
128 after = [ "network-online.target" ];
129 restartTriggers = [ config.environment.etc."nomad.json".source ];
130
131 path = cfg.extraPackages ++ (with pkgs; [
132 # Client mode requires at least the following:
133 coreutils
134 iproute2
135 iptables
136 ]);
137
138 serviceConfig = mkMerge [
139 {
140 DynamicUser = cfg.dropPrivileges;
141 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
142 ExecStart = "${cfg.package}/bin/nomad agent -config=/etc/nomad.json" +
143 concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths +
144 concatMapStrings (path: " -plugin-dir=${path}/bin") cfg.extraSettingsPlugins;
145 KillMode = "process";
146 KillSignal = "SIGINT";
147 LimitNOFILE = 65536;
148 LimitNPROC = "infinity";
149 OOMScoreAdjust = -1000;
150 Restart = "on-failure";
151 RestartSec = 2;
152 TasksMax = "infinity";
153 }
154 (mkIf cfg.enableDocker {
155 SupplementaryGroups = "docker"; # space-separated string
156 })
157 (mkIf (cfg.settings.data_dir == "/var/lib/nomad") {
158 StateDirectory = "nomad";
159 })
160 ];
161
162 unitConfig = {
163 StartLimitIntervalSec = 10;
164 StartLimitBurst = 3;
165 };
166 };
167
168 assertions = [
169 {
170 assertion = cfg.dropPrivileges -> cfg.settings.data_dir == "/var/lib/nomad";
171 message = "settings.data_dir must be equal to \"/var/lib/nomad\" if dropPrivileges is true";
172 }
173 ];
174
175 # Docker support requires the Docker daemon to be running.
176 virtualisation.docker.enable = mkIf cfg.enableDocker true;
177 };
178}