1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.k3s;
9 removeOption =
10 config: instruction:
11 lib.mkRemovedOptionModule (
12 [
13 "services"
14 "k3s"
15 ]
16 ++ config
17 ) instruction;
18
19 manifestDir = "/var/lib/rancher/k3s/server/manifests";
20 chartDir = "/var/lib/rancher/k3s/server/static/charts";
21 imageDir = "/var/lib/rancher/k3s/agent/images";
22 containerdConfigTemplateFile = "/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl";
23 yamlFormat = pkgs.formats.yaml { };
24 yamlDocSeparator = builtins.toFile "yaml-doc-separator" "\n---\n";
25 # Manifests need a valid YAML suffix to be respected by k3s
26 mkManifestTarget =
27 name: if (lib.hasSuffix ".yaml" name || lib.hasSuffix ".yml" name) then name else name + ".yaml";
28 # Produces a list containing all duplicate manifest names
29 duplicateManifests =
30 with builtins;
31 lib.intersectLists (attrNames cfg.autoDeployCharts) (attrNames cfg.manifests);
32 # Produces a list containing all duplicate chart names
33 duplicateCharts =
34 with builtins;
35 lib.intersectLists (attrNames cfg.autoDeployCharts) (attrNames cfg.charts);
36
37 # Converts YAML -> JSON -> Nix
38 fromYaml =
39 path:
40 with builtins;
41 fromJSON (
42 readFile (
43 pkgs.runCommand "${path}-converted.json" { nativeBuildInputs = [ yq-go ]; } ''
44 yq --no-colors --output-format json ${path} > $out
45 ''
46 )
47 );
48
49 # Replace characters that are problematic in file names
50 cleanHelmChartName =
51 lib.replaceStrings
52 [
53 "/"
54 ":"
55 ]
56 [
57 "-"
58 "-"
59 ];
60
61 # Fetch a Helm chart from a public registry. This only supports a basic Helm pull.
62 fetchHelm =
63 {
64 name,
65 repo,
66 version,
67 hash ? lib.fakeHash,
68 }:
69 pkgs.runCommand (cleanHelmChartName "${lib.removePrefix "https://" repo}-${name}-${version}.tgz")
70 {
71 inherit (lib.fetchers.normalizeHash { } { inherit hash; }) outputHash outputHashAlgo;
72 impureEnvVars = lib.fetchers.proxyImpureEnvVars;
73 nativeBuildInputs = with pkgs; [
74 kubernetes-helm
75 cacert
76 ];
77 }
78 ''
79 export HOME="$PWD"
80 helm repo add repository ${repo}
81 helm pull repository/${name} --version ${version}
82 mv ./*.tgz $out
83 '';
84
85 # Returns the path to a YAML manifest file
86 mkExtraDeployManifest =
87 x:
88 # x is a derivation that provides a YAML file
89 if lib.isDerivation x then
90 x.outPath
91 # x is an attribute set that needs to be converted to a YAML file
92 else if builtins.isAttrs x then
93 (yamlFormat.generate "extra-deploy-chart-manifest" x)
94 # assume x is a path to a YAML file
95 else
96 x;
97
98 # Generate a HelmChart custom resource.
99 mkHelmChartCR =
100 name: value:
101 let
102 chartValues = if (lib.isPath value.values) then fromYaml value.values else value.values;
103 # use JSON for values as it's a subset of YAML and understood by the k3s Helm controller
104 valuesContent = builtins.toJSON chartValues;
105 in
106 # merge with extraFieldDefinitions to allow setting advanced values and overwrite generated
107 # values
108 lib.recursiveUpdate {
109 apiVersion = "helm.cattle.io/v1";
110 kind = "HelmChart";
111 metadata = {
112 inherit name;
113 namespace = "kube-system";
114 };
115 spec = {
116 inherit valuesContent;
117 inherit (value) targetNamespace createNamespace;
118 chart = "https://%{KUBERNETES_API}%/static/charts/${name}.tgz";
119 };
120 } value.extraFieldDefinitions;
121
122 # Generate a HelmChart custom resource together with extraDeploy manifests. This
123 # generates possibly a multi document YAML file that the auto deploy mechanism of k3s
124 # deploys.
125 mkAutoDeployChartManifest = name: value: {
126 # target is the final name of the link created for the manifest file
127 target = mkManifestTarget name;
128 inherit (value) enable package;
129 # source is a store path containing the complete manifest file
130 source = pkgs.concatText "auto-deploy-chart-${name}.yaml" (
131 [
132 (yamlFormat.generate "helm-chart-manifest-${name}.yaml" (mkHelmChartCR name value))
133 ]
134 # alternate the YAML doc seperator (---) and extraDeploy manifests to create
135 # multi document YAMLs
136 ++ (lib.concatMap (x: [
137 yamlDocSeparator
138 (mkExtraDeployManifest x)
139 ]) value.extraDeploy)
140 );
141 };
142
143 autoDeployChartsModule = lib.types.submodule (
144 { config, ... }:
145 {
146 options = {
147 enable = lib.mkOption {
148 type = lib.types.bool;
149 default = true;
150 example = false;
151 description = ''
152 Whether to enable the installation of this Helm chart. Note that setting
153 this option to `false` will not uninstall the chart from the cluster, if
154 it was previously installed. Please use the the `--disable` flag or `.skip`
155 files to delete/disable Helm charts, as mentioned in the
156 [docs](https://docs.k3s.io/installation/packaged-components#disabling-manifests).
157 '';
158 };
159
160 repo = lib.mkOption {
161 type = lib.types.nonEmptyStr;
162 example = "https://kubernetes.github.io/ingress-nginx";
163 description = ''
164 The repo of the Helm chart. Only has an effect if `package` is not set.
165 The Helm chart is fetched during build time and placed as a `.tgz` archive on the
166 filesystem.
167 '';
168 };
169
170 name = lib.mkOption {
171 type = lib.types.nonEmptyStr;
172 example = "ingress-nginx";
173 description = ''
174 The name of the Helm chart. Only has an effect if `package` is not set.
175 The Helm chart is fetched during build time and placed as a `.tgz` archive on the
176 filesystem.
177 '';
178 };
179
180 version = lib.mkOption {
181 type = lib.types.nonEmptyStr;
182 example = "4.7.0";
183 description = ''
184 The version of the Helm chart. Only has an effect if `package` is not set.
185 The Helm chart is fetched during build time and placed as a `.tgz` archive on the
186 filesystem.
187 '';
188 };
189
190 hash = lib.mkOption {
191 type = lib.types.str;
192 example = "sha256-ej+vpPNdiOoXsaj1jyRpWLisJgWo8EqX+Z5VbpSjsPA=";
193 default = "";
194 description = ''
195 The hash of the packaged Helm chart. Only has an effect if `package` is not set.
196 The Helm chart is fetched during build time and placed as a `.tgz` archive on the
197 filesystem.
198 '';
199 };
200
201 package = lib.mkOption {
202 type = with lib.types; either path package;
203 example = lib.literalExpression "../my-helm-chart.tgz";
204 description = ''
205 The packaged Helm chart. Overwrites the options `repo`, `name`, `version`
206 and `hash` in case of conflicts.
207 '';
208 };
209
210 targetNamespace = lib.mkOption {
211 type = lib.types.nonEmptyStr;
212 default = "default";
213 example = "kube-system";
214 description = "The namespace in which the Helm chart gets installed.";
215 };
216
217 createNamespace = lib.mkOption {
218 type = lib.types.bool;
219 default = false;
220 example = true;
221 description = "Whether to create the target namespace if not present.";
222 };
223
224 values = lib.mkOption {
225 type = with lib.types; either path attrs;
226 default = { };
227 example = {
228 replicaCount = 3;
229 hostName = "my-host";
230 server = {
231 name = "nginx";
232 port = 80;
233 };
234 };
235 description = ''
236 Override default chart values via Nix expressions. This is equivalent to setting
237 values in a `values.yaml` file.
238
239 WARNING: The values (including secrets!) specified here are exposed unencrypted
240 in the world-readable nix store.
241 '';
242 };
243
244 extraDeploy = lib.mkOption {
245 type = with lib.types; listOf (either path attrs);
246 default = [ ];
247 example = lib.literalExpression ''
248 [
249 ../manifests/my-extra-deployment.yaml
250 {
251 apiVersion = "v1";
252 kind = "Service";
253 metadata = {
254 name = "app-service";
255 };
256 spec = {
257 selector = {
258 "app.kubernetes.io/name" = "MyApp";
259 };
260 ports = [
261 {
262 name = "name-of-service-port";
263 protocol = "TCP";
264 port = 80;
265 targetPort = "http-web-svc";
266 }
267 ];
268 };
269 }
270 ];
271 '';
272 description = "List of extra Kubernetes manifests to deploy with this Helm chart.";
273 };
274
275 extraFieldDefinitions = lib.mkOption {
276 inherit (yamlFormat) type;
277 default = { };
278 example = {
279 spec = {
280 bootstrap = true;
281 helmVersion = "v2";
282 backOffLimit = 3;
283 jobImage = "custom-helm-controller:v0.0.1";
284 };
285 };
286 description = ''
287 Extra HelmChart field definitions that are merged with the rest of the HelmChart
288 custom resource. This can be used to set advanced fields or to overwrite
289 generated fields. See https://docs.k3s.io/helm#helmchart-field-definitions
290 for possible fields.
291 '';
292 };
293 };
294
295 config.package = lib.mkDefault (fetchHelm {
296 inherit (config)
297 repo
298 name
299 version
300 hash
301 ;
302 });
303 }
304 );
305
306 manifestModule = lib.types.submodule (
307 {
308 name,
309 config,
310 options,
311 ...
312 }:
313 {
314 options = {
315 enable = lib.mkOption {
316 type = lib.types.bool;
317 default = true;
318 description = "Whether this manifest file should be generated.";
319 };
320
321 target = lib.mkOption {
322 type = lib.types.nonEmptyStr;
323 example = "manifest.yaml";
324 description = ''
325 Name of the symlink (relative to {file}`${manifestDir}`).
326 Defaults to the attribute name.
327 '';
328 };
329
330 content = lib.mkOption {
331 type = with lib.types; nullOr (either attrs (listOf attrs));
332 default = null;
333 description = ''
334 Content of the manifest file. A single attribute set will
335 generate a single document YAML file. A list of attribute sets
336 will generate multiple documents separated by `---` in a single
337 YAML file.
338 '';
339 };
340
341 source = lib.mkOption {
342 type = lib.types.path;
343 example = lib.literalExpression "./manifests/app.yaml";
344 description = ''
345 Path of the source `.yaml` file.
346 '';
347 };
348 };
349
350 config = {
351 target = lib.mkDefault (mkManifestTarget name);
352 source = lib.mkIf (config.content != null) (
353 let
354 name' = "k3s-manifest-" + builtins.baseNameOf name;
355 docName = "k3s-manifest-doc-" + builtins.baseNameOf name;
356 mkSource =
357 value:
358 if builtins.isList value then
359 pkgs.concatText name' (
360 lib.concatMap (x: [
361 yamlDocSeparator
362 (yamlFormat.generate docName x)
363 ]) value
364 )
365 else
366 yamlFormat.generate name' value;
367 in
368 lib.mkDerivedConfig options.content mkSource
369 );
370 };
371 }
372 );
373in
374{
375 imports = [ (removeOption [ "docker" ] "k3s docker option is no longer supported.") ];
376
377 # interface
378 options.services.k3s = {
379 enable = lib.mkEnableOption "k3s";
380
381 package = lib.mkPackageOption pkgs "k3s" { };
382
383 role = lib.mkOption {
384 description = ''
385 Whether k3s should run as a server or agent.
386
387 If it's a server:
388
389 - By default it also runs workloads as an agent.
390 - Starts by default as a standalone server using an embedded sqlite datastore.
391 - Configure `clusterInit = true` to switch over to embedded etcd datastore and enable HA mode.
392 - Configure `serverAddr` to join an already-initialized HA cluster.
393
394 If it's an agent:
395
396 - `serverAddr` is required.
397 '';
398 default = "server";
399 type = lib.types.enum [
400 "server"
401 "agent"
402 ];
403 };
404
405 serverAddr = lib.mkOption {
406 type = lib.types.str;
407 description = ''
408 The k3s server to connect to.
409
410 Servers and agents need to communicate each other. Read
411 [the networking docs](https://rancher.com/docs/k3s/latest/en/installation/installation-requirements/#networking)
412 to know how to configure the firewall.
413 '';
414 example = "https://10.0.0.10:6443";
415 default = "";
416 };
417
418 clusterInit = lib.mkOption {
419 type = lib.types.bool;
420 default = false;
421 description = ''
422 Initialize HA cluster using an embedded etcd datastore.
423
424 If this option is `false` and `role` is `server`
425
426 On a server that was using the default embedded sqlite backend,
427 enabling this option will migrate to an embedded etcd DB.
428
429 If an HA cluster using the embedded etcd datastore was already initialized,
430 this option has no effect.
431
432 This option only makes sense in a server that is not connecting to another server.
433
434 If you are configuring an HA cluster with an embedded etcd,
435 the 1st server must have `clusterInit = true`
436 and other servers must connect to it using `serverAddr`.
437 '';
438 };
439
440 token = lib.mkOption {
441 type = lib.types.str;
442 description = ''
443 The k3s token to use when connecting to a server.
444
445 WARNING: This option will expose store your token unencrypted world-readable in the nix store.
446 If this is undesired use the tokenFile option instead.
447 '';
448 default = "";
449 };
450
451 tokenFile = lib.mkOption {
452 type = lib.types.nullOr lib.types.path;
453 description = "File path containing k3s token to use when connecting to the server.";
454 default = null;
455 };
456
457 extraFlags = lib.mkOption {
458 description = "Extra flags to pass to the k3s command.";
459 type = with lib.types; either str (listOf str);
460 default = [ ];
461 example = [
462 "--disable traefik"
463 "--cluster-cidr 10.24.0.0/16"
464 ];
465 };
466
467 disableAgent = lib.mkOption {
468 type = lib.types.bool;
469 default = false;
470 description = "Only run the server. This option only makes sense for a server.";
471 };
472
473 environmentFile = lib.mkOption {
474 type = lib.types.nullOr lib.types.path;
475 description = ''
476 File path containing environment variables for configuring the k3s service in the format of an EnvironmentFile. See {manpage}`systemd.exec(5)`.
477 '';
478 default = null;
479 };
480
481 configPath = lib.mkOption {
482 type = lib.types.nullOr lib.types.path;
483 default = null;
484 description = "File path containing the k3s YAML config. This is useful when the config is generated (for example on boot).";
485 };
486
487 manifests = lib.mkOption {
488 type = lib.types.attrsOf manifestModule;
489 default = { };
490 example = lib.literalExpression ''
491 {
492 deployment.source = ../manifests/deployment.yaml;
493 my-service = {
494 enable = false;
495 target = "app-service.yaml";
496 content = {
497 apiVersion = "v1";
498 kind = "Service";
499 metadata = {
500 name = "app-service";
501 };
502 spec = {
503 selector = {
504 "app.kubernetes.io/name" = "MyApp";
505 };
506 ports = [
507 {
508 name = "name-of-service-port";
509 protocol = "TCP";
510 port = 80;
511 targetPort = "http-web-svc";
512 }
513 ];
514 };
515 };
516 };
517
518 nginx.content = [
519 {
520 apiVersion = "v1";
521 kind = "Pod";
522 metadata = {
523 name = "nginx";
524 labels = {
525 "app.kubernetes.io/name" = "MyApp";
526 };
527 };
528 spec = {
529 containers = [
530 {
531 name = "nginx";
532 image = "nginx:1.14.2";
533 ports = [
534 {
535 containerPort = 80;
536 name = "http-web-svc";
537 }
538 ];
539 }
540 ];
541 };
542 }
543 {
544 apiVersion = "v1";
545 kind = "Service";
546 metadata = {
547 name = "nginx-service";
548 };
549 spec = {
550 selector = {
551 "app.kubernetes.io/name" = "MyApp";
552 };
553 ports = [
554 {
555 name = "name-of-service-port";
556 protocol = "TCP";
557 port = 80;
558 targetPort = "http-web-svc";
559 }
560 ];
561 };
562 }
563 ];
564 };
565 '';
566 description = ''
567 Auto-deploying manifests that are linked to {file}`${manifestDir}` before k3s starts.
568 Note that deleting manifest files will not remove or otherwise modify the resources
569 it created. Please use the the `--disable` flag or `.skip` files to delete/disable AddOns,
570 as mentioned in the [docs](https://docs.k3s.io/installation/packaged-components#disabling-manifests).
571 This option only makes sense on server nodes (`role = server`).
572 Read the [auto-deploying manifests docs](https://docs.k3s.io/installation/packaged-components#auto-deploying-manifests-addons)
573 for further information.
574 '';
575 };
576
577 charts = lib.mkOption {
578 type = with lib.types; attrsOf (either path package);
579 default = { };
580 example = lib.literalExpression ''
581 nginx = ../charts/my-nginx-chart.tgz;
582 redis = ../charts/my-redis-chart.tgz;
583 '';
584 description = ''
585 Packaged Helm charts that are linked to {file}`${chartDir}` before k3s starts.
586 The attribute name will be used as the link target (relative to {file}`${chartDir}`).
587 The specified charts will only be placed on the file system and made available to the
588 Kubernetes APIServer from within the cluster. See the [](#opt-services.k3s.autoDeployCharts)
589 option and the [k3s Helm controller docs](https://docs.k3s.io/helm#using-the-helm-controller)
590 to deploy Helm charts. This option only makes sense on server nodes (`role = server`).
591 '';
592 };
593
594 containerdConfigTemplate = lib.mkOption {
595 type = lib.types.nullOr lib.types.str;
596 default = null;
597 example = lib.literalExpression ''
598 # Base K3s config
599 {{ template "base" . }}
600
601 # Add a custom runtime
602 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes."custom"]
603 runtime_type = "io.containerd.runc.v2"
604 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes."custom".options]
605 BinaryName = "/path/to/custom-container-runtime"
606 '';
607 description = ''
608 Config template for containerd, to be placed at
609 `/var/lib/rancher/k3s/agent/etc/containerd/config.toml.tmpl`.
610 See the K3s docs on [configuring containerd](https://docs.k3s.io/advanced#configuring-containerd).
611 '';
612 };
613
614 images = lib.mkOption {
615 type = with lib.types; listOf package;
616 default = [ ];
617 example = lib.literalExpression ''
618 [
619 (pkgs.dockerTools.pullImage {
620 imageName = "docker.io/bitnami/keycloak";
621 imageDigest = "sha256:714dfadc66a8e3adea6609bda350345bd3711657b7ef3cf2e8015b526bac2d6b";
622 hash = "sha256-IM2BLZ0EdKIZcRWOtuFY9TogZJXCpKtPZnMnPsGlq0Y=";
623 finalImageTag = "21.1.2-debian-11-r0";
624 })
625
626 config.services.k3s.package.airgapImages
627 ]
628 '';
629 description = ''
630 List of derivations that provide container images.
631 All images are linked to {file}`${imageDir}` before k3s starts and consequently imported
632 by the k3s agent. Consider importing the k3s airgap images archive of the k3s package in
633 use, if you want to pre-provision this node with all k3s container images. This option
634 only makes sense on nodes with an enabled agent.
635 '';
636 };
637
638 gracefulNodeShutdown = {
639 enable = lib.mkEnableOption ''
640 graceful node shutdowns where the kubelet attempts to detect
641 node system shutdown and terminates pods running on the node. See the
642 [documentation](https://kubernetes.io/docs/concepts/cluster-administration/node-shutdown/#graceful-node-shutdown)
643 for further information.
644 '';
645
646 shutdownGracePeriod = lib.mkOption {
647 type = lib.types.nonEmptyStr;
648 default = "30s";
649 example = "1m30s";
650 description = ''
651 Specifies the total duration that the node should delay the shutdown by. This is the total
652 grace period for pod termination for both regular and critical pods.
653 '';
654 };
655
656 shutdownGracePeriodCriticalPods = lib.mkOption {
657 type = lib.types.nonEmptyStr;
658 default = "10s";
659 example = "15s";
660 description = ''
661 Specifies the duration used to terminate critical pods during a node shutdown. This should be
662 less than `shutdownGracePeriod`.
663 '';
664 };
665 };
666
667 extraKubeletConfig = lib.mkOption {
668 type = with lib.types; attrsOf anything;
669 default = { };
670 example = {
671 podsPerCore = 3;
672 memoryThrottlingFactor = 0.69;
673 containerLogMaxSize = "5Mi";
674 };
675 description = ''
676 Extra configuration to add to the kubelet's configuration file. The subset of the kubelet's
677 configuration that can be configured via a file is defined by the
678 [KubeletConfiguration](https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/)
679 struct. See the
680 [documentation](https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/)
681 for further information.
682 '';
683 };
684
685 extraKubeProxyConfig = lib.mkOption {
686 type = with lib.types; attrsOf anything;
687 default = { };
688 example = {
689 mode = "nftables";
690 clientConnection.kubeconfig = "/var/lib/rancher/k3s/agent/kubeproxy.kubeconfig";
691 };
692 description = ''
693 Extra configuration to add to the kube-proxy's configuration file. The subset of the kube-proxy's
694 configuration that can be configured via a file is defined by the
695 [KubeProxyConfiguration](https://kubernetes.io/docs/reference/config-api/kube-proxy-config.v1alpha1/)
696 struct. Note that the kubeconfig param will be override by `clientConnection.kubeconfig`, so you must
697 set the `clientConnection.kubeconfig` if you want to use `extraKubeProxyConfig`.
698 '';
699 };
700
701 autoDeployCharts = lib.mkOption {
702 type = lib.types.attrsOf autoDeployChartsModule;
703 apply = lib.mapAttrs mkAutoDeployChartManifest;
704 default = { };
705 example = lib.literalExpression ''
706 {
707 harbor = {
708 name = "harbor";
709 repo = "https://helm.goharbor.io";
710 version = "1.14.0";
711 hash = "sha256-fMP7q1MIbvzPGS9My91vbQ1d3OJMjwc+o8YE/BXZaYU=";
712 values = {
713 existingSecretAdminPassword = "harbor-admin";
714 expose = {
715 tls = {
716 enabled = true;
717 certSource = "secret";
718 secret.secretName = "my-tls-secret";
719 };
720 ingress = {
721 hosts.core = "example.com";
722 className = "nginx";
723 };
724 };
725 };
726 };
727
728 custom-chart = {
729 package = ../charts/my-chart.tgz;
730 values = ../values/my-values.yaml;
731 extraFieldDefinitions = {
732 spec.timeout = "60s";
733 };
734 };
735 }
736 '';
737 description = ''
738 Auto deploying Helm charts that are installed by the k3s Helm controller. Avoid to use
739 attribute names that are also used in the [](#opt-services.k3s.manifests) and
740 [](#opt-services.k3s.charts) options. Manifests with the same name will override
741 auto deploying charts with the same name. Similiarly, charts with the same name will
742 overwrite the Helm chart contained in auto deploying charts. This option only makes
743 sense on server nodes (`role = server`). See the
744 [k3s Helm documentation](https://docs.k3s.io/helm) for further information.
745 '';
746 };
747 };
748
749 # implementation
750
751 config = lib.mkIf cfg.enable {
752 warnings =
753 (lib.optional (cfg.role != "server" && cfg.manifests != { })
754 "k3s: Auto deploying manifests are only installed on server nodes (role == server), they will be ignored by this node."
755 )
756 ++ (lib.optional (cfg.role != "server" && cfg.charts != { })
757 "k3s: Helm charts are only made available to the cluster on server nodes (role == server), they will be ignored by this node."
758 )
759 ++ (lib.optional (cfg.role != "server" && cfg.autoDeployCharts != { })
760 "k3s: Auto deploying Helm charts are only installed on server nodes (role == server), they will be ignored by this node."
761 )
762 ++ (lib.optional (duplicateManifests != [ ])
763 "k3s: The following auto deploying charts are overriden by manifests of the same name: ${toString duplicateManifests}."
764 )
765 ++ (lib.optional (duplicateCharts != [ ])
766 "k3s: The following auto deploying charts are overriden by charts of the same name: ${toString duplicateCharts}."
767 )
768 ++ (lib.optional (
769 cfg.disableAgent && cfg.images != [ ]
770 ) "k3s: Images are only imported on nodes with an enabled agent, they will be ignored by this node")
771 ++ (lib.optional (
772 cfg.role == "agent" && cfg.configPath == null && cfg.serverAddr == ""
773 ) "k3s: ServerAddr or configPath (with 'server' key) should be set if role is 'agent'")
774 ++ (lib.optional
775 (cfg.role == "agent" && cfg.configPath == null && cfg.tokenFile == null && cfg.token == "")
776 "k3s: Token or tokenFile or configPath (with 'token' or 'token-file' keys) should be set if role is 'agent'"
777 );
778
779 assertions = [
780 {
781 assertion = cfg.role == "agent" -> !cfg.disableAgent;
782 message = "k3s: disableAgent must be false if role is 'agent'";
783 }
784 {
785 assertion = cfg.role == "agent" -> !cfg.clusterInit;
786 message = "k3s: clusterInit must be false if role is 'agent'";
787 }
788 ];
789
790 environment.systemPackages = [ config.services.k3s.package ];
791
792 # Use systemd-tmpfiles to activate k3s content
793 systemd.tmpfiles.settings."10-k3s" =
794 let
795 # Merge manifest with manifests generated from auto deploying charts, keep only enabled manifests
796 enabledManifests = lib.filterAttrs (_: v: v.enable) (cfg.autoDeployCharts // cfg.manifests);
797 # Merge charts with charts contained in enabled auto deploying charts
798 helmCharts =
799 (lib.concatMapAttrs (n: v: { ${n} = v.package; }) (
800 lib.filterAttrs (_: v: v.enable) cfg.autoDeployCharts
801 ))
802 // cfg.charts;
803 # Make a systemd-tmpfiles rule for a manifest
804 mkManifestRule = manifest: {
805 name = "${manifestDir}/${manifest.target}";
806 value = {
807 "L+".argument = "${manifest.source}";
808 };
809 };
810 # Ensure that all chart targets have a .tgz suffix
811 mkChartTarget = name: if (lib.hasSuffix ".tgz" name) then name else name + ".tgz";
812 # Make a systemd-tmpfiles rule for a chart
813 mkChartRule = target: source: {
814 name = "${chartDir}/${mkChartTarget target}";
815 value = {
816 "L+".argument = "${source}";
817 };
818 };
819 # Make a systemd-tmpfiles rule for a container image
820 mkImageRule = image: {
821 name = "${imageDir}/${image.name}";
822 value = {
823 "L+".argument = "${image}";
824 };
825 };
826 in
827 (lib.mapAttrs' (_: v: mkManifestRule v) enabledManifests)
828 // (lib.mapAttrs' (n: v: mkChartRule n v) helmCharts)
829 // (builtins.listToAttrs (map mkImageRule cfg.images))
830 // (lib.optionalAttrs (cfg.containerdConfigTemplate != null) {
831 ${containerdConfigTemplateFile} = {
832 "L+".argument = "${pkgs.writeText "config.toml.tmpl" cfg.containerdConfigTemplate}";
833 };
834 });
835
836 systemd.services.k3s =
837 let
838 kubeletParams =
839 (lib.optionalAttrs (cfg.gracefulNodeShutdown.enable) {
840 inherit (cfg.gracefulNodeShutdown) shutdownGracePeriod shutdownGracePeriodCriticalPods;
841 })
842 // cfg.extraKubeletConfig;
843 kubeletConfig = (pkgs.formats.yaml { }).generate "k3s-kubelet-config" (
844 {
845 apiVersion = "kubelet.config.k8s.io/v1beta1";
846 kind = "KubeletConfiguration";
847 }
848 // kubeletParams
849 );
850
851 kubeProxyConfig = (pkgs.formats.yaml { }).generate "k3s-kubeProxy-config" (
852 {
853 apiVersion = "kubeproxy.config.k8s.io/v1alpha1";
854 kind = "KubeProxyConfiguration";
855 }
856 // cfg.extraKubeProxyConfig
857 );
858 in
859 {
860 description = "k3s service";
861 after = [
862 "firewall.service"
863 "network-online.target"
864 ];
865 wants = [
866 "firewall.service"
867 "network-online.target"
868 ];
869 wantedBy = [ "multi-user.target" ];
870 path = lib.optional config.boot.zfs.enabled config.boot.zfs.package;
871 serviceConfig = {
872 # See: https://github.com/rancher/k3s/blob/dddbd16305284ae4bd14c0aade892412310d7edc/install.sh#L197
873 Type = if cfg.role == "agent" then "exec" else "notify";
874 KillMode = "process";
875 Delegate = "yes";
876 Restart = "always";
877 RestartSec = "5s";
878 LimitNOFILE = 1048576;
879 LimitNPROC = "infinity";
880 LimitCORE = "infinity";
881 TasksMax = "infinity";
882 EnvironmentFile = cfg.environmentFile;
883 ExecStart = lib.concatStringsSep " \\\n " (
884 [ "${cfg.package}/bin/k3s ${cfg.role}" ]
885 ++ (lib.optional cfg.clusterInit "--cluster-init")
886 ++ (lib.optional cfg.disableAgent "--disable-agent")
887 ++ (lib.optional (cfg.serverAddr != "") "--server ${cfg.serverAddr}")
888 ++ (lib.optional (cfg.token != "") "--token ${cfg.token}")
889 ++ (lib.optional (cfg.tokenFile != null) "--token-file ${cfg.tokenFile}")
890 ++ (lib.optional (cfg.configPath != null) "--config ${cfg.configPath}")
891 ++ (lib.optional (kubeletParams != { }) "--kubelet-arg=config=${kubeletConfig}")
892 ++ (lib.optional (cfg.extraKubeProxyConfig != { }) "--kube-proxy-arg=config=${kubeProxyConfig}")
893 ++ (lib.flatten cfg.extraFlags)
894 );
895 };
896 };
897 };
898
899 meta.maintainers = lib.teams.k3s.members;
900}