1{ config, lib, pkgs, ... }:
2
3with lib;
4let
5 cfg = config.services.k3s;
6 removeOption = config: instruction:
7 lib.mkRemovedOptionModule ([ "services" "k3s" ] ++ config) instruction;
8in
9{
10 imports = [
11 (removeOption [ "docker" ] "k3s docker option is no longer supported.")
12 ];
13
14 # interface
15 options.services.k3s = {
16 enable = mkEnableOption (lib.mdDoc "k3s");
17
18 package = mkOption {
19 type = types.package;
20 default = pkgs.k3s;
21 defaultText = literalExpression "pkgs.k3s";
22 description = lib.mdDoc "Package that should be used for k3s";
23 };
24
25 role = mkOption {
26 description = lib.mdDoc ''
27 Whether k3s should run as a server or agent.
28
29 If it's a server:
30
31 - By default it also runs workloads as an agent.
32 - Starts by default as a standalone server using an embedded sqlite datastore.
33 - Configure `clusterInit = true` to switch over to embedded etcd datastore and enable HA mode.
34 - Configure `serverAddr` to join an already-initialized HA cluster.
35
36 If it's an agent:
37
38 - `serverAddr` is required.
39 '';
40 default = "server";
41 type = types.enum [ "server" "agent" ];
42 };
43
44 serverAddr = mkOption {
45 type = types.str;
46 description = lib.mdDoc ''
47 The k3s server to connect to.
48
49 Servers and agents need to communicate each other. Read
50 [the networking docs](https://rancher.com/docs/k3s/latest/en/installation/installation-requirements/#networking)
51 to know how to configure the firewall.
52 '';
53 example = "https://10.0.0.10:6443";
54 default = "";
55 };
56
57 clusterInit = mkOption {
58 type = types.bool;
59 default = false;
60 description = lib.mdDoc ''
61 Initialize HA cluster using an embedded etcd datastore.
62
63 If this option is `false` and `role` is `server`
64
65 On a server that was using the default embedded sqlite backend,
66 enabling this option will migrate to an embedded etcd DB.
67
68 If an HA cluster using the embedded etcd datastore was already initialized,
69 this option has no effect.
70
71 This option only makes sense in a server that is not connecting to another server.
72
73 If you are configuring an HA cluster with an embedded etcd,
74 the 1st server must have `clusterInit = true`
75 and other servers must connect to it using `serverAddr`.
76 '';
77 };
78
79 token = mkOption {
80 type = types.str;
81 description = lib.mdDoc ''
82 The k3s token to use when connecting to a server.
83
84 WARNING: This option will expose store your token unencrypted world-readable in the nix store.
85 If this is undesired use the tokenFile option instead.
86 '';
87 default = "";
88 };
89
90 tokenFile = mkOption {
91 type = types.nullOr types.path;
92 description = lib.mdDoc "File path containing k3s token to use when connecting to the server.";
93 default = null;
94 };
95
96 extraFlags = mkOption {
97 description = lib.mdDoc "Extra flags to pass to the k3s command.";
98 type = types.str;
99 default = "";
100 example = "--no-deploy traefik --cluster-cidr 10.24.0.0/16";
101 };
102
103 disableAgent = mkOption {
104 type = types.bool;
105 default = false;
106 description = lib.mdDoc "Only run the server. This option only makes sense for a server.";
107 };
108
109 configPath = mkOption {
110 type = types.nullOr types.path;
111 default = null;
112 description = lib.mdDoc "File path containing the k3s YAML config. This is useful when the config is generated (for example on boot).";
113 };
114 };
115
116 # implementation
117
118 config = mkIf cfg.enable {
119 assertions = [
120 {
121 assertion = cfg.role == "agent" -> (cfg.configPath != null || cfg.serverAddr != "");
122 message = "serverAddr or configPath (with 'server' key) should be set if role is 'agent'";
123 }
124 {
125 assertion = cfg.role == "agent" -> cfg.configPath != null || cfg.tokenFile != null || cfg.token != "";
126 message = "token or tokenFile or configPath (with 'token' or 'token-file' keys) should be set if role is 'agent'";
127 }
128 {
129 assertion = cfg.role == "agent" -> !cfg.disableAgent;
130 message = "disableAgent must be false if role is 'agent'";
131 }
132 {
133 assertion = cfg.role == "agent" -> !cfg.clusterInit;
134 message = "clusterInit must be false if role is 'agent'";
135 }
136 ];
137
138 environment.systemPackages = [ config.services.k3s.package ];
139
140 systemd.services.k3s = {
141 description = "k3s service";
142 after = [ "network.service" "firewall.service" ];
143 wants = [ "network.service" "firewall.service" ];
144 wantedBy = [ "multi-user.target" ];
145 path = optional config.boot.zfs.enabled config.boot.zfs.package;
146 serviceConfig = {
147 # See: https://github.com/rancher/k3s/blob/dddbd16305284ae4bd14c0aade892412310d7edc/install.sh#L197
148 Type = if cfg.role == "agent" then "exec" else "notify";
149 KillMode = "process";
150 Delegate = "yes";
151 Restart = "always";
152 RestartSec = "5s";
153 LimitNOFILE = 1048576;
154 LimitNPROC = "infinity";
155 LimitCORE = "infinity";
156 TasksMax = "infinity";
157 ExecStart = concatStringsSep " \\\n " (
158 [
159 "${cfg.package}/bin/k3s ${cfg.role}"
160 ]
161 ++ (optional cfg.clusterInit "--cluster-init")
162 ++ (optional cfg.disableAgent "--disable-agent")
163 ++ (optional (cfg.serverAddr != "") "--server ${cfg.serverAddr}")
164 ++ (optional (cfg.token != "") "--token ${cfg.token}")
165 ++ (optional (cfg.tokenFile != null) "--token-file ${cfg.tokenFile}")
166 ++ (optional (cfg.configPath != null) "--config ${cfg.configPath}")
167 ++ [ cfg.extraFlags ]
168 );
169 };
170 };
171 };
172}