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