nixos/k3s: add `autoDeployCharts` option and use systemd-tmpfiles for content activation (#374017)

authored by Marcus Ramberg and committed by GitHub d07ebbab 626f9686

+689 -161
+2
nixos/doc/manual/release-notes/rl-2505.section.md
··· 597 - New options for the declarative configuration of the user space part of ALSA have been introduced under [hardware.alsa](options.html#opt-hardware.alsa.enable), including setting the default capture and playback device, defining sound card aliases and volume controls. 598 Note: these are intended for users not running a sound server like PulseAudio or PipeWire, but having ALSA as their only sound system. 599 600 - Caddy can now be built with plugins by using `caddy.withPlugins`, a `passthru` function that accepts an attribute set as a parameter. The `plugins` argument represents a list of Caddy plugins, with each Caddy plugin being a versioned module. The `hash` argument represents the `vendorHash` of the resulting Caddy source code with the plugins added. 601 602 Example:
··· 597 - New options for the declarative configuration of the user space part of ALSA have been introduced under [hardware.alsa](options.html#opt-hardware.alsa.enable), including setting the default capture and playback device, defining sound card aliases and volume controls. 598 Note: these are intended for users not running a sound server like PulseAudio or PipeWire, but having ALSA as their only sound system. 599 600 + - `services.k3s` now provides the `autoDeployCharts` option that allows to automatically deploy Helm charts via the k3s Helm controller. 601 + 602 - Caddy can now be built with plugins by using `caddy.withPlugins`, a `passthru` function that accepts an attribute set as a parameter. The `plugins` argument represents a list of Caddy plugins, with each Caddy plugin being a versioned module. The `hash` argument represents the `vendorHash` of the resulting Caddy source code with the plugins added. 603 604 Example:
+506 -161
nixos/modules/services/cluster/k3s/default.nix
··· 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 24 - manifestModule = 25 let 26 - mkTarget = 27 - name: if (lib.hasSuffix ".yaml" name || lib.hasSuffix ".yml" name) then name else name + ".yaml"; 28 in 29 - lib.types.submodule ( 30 - { 31 - name, 32 - config, 33 - options, 34 - ... 35 - }: 36 - { 37 - options = { 38 - enable = lib.mkOption { 39 - type = lib.types.bool; 40 - default = true; 41 - description = "Whether this manifest file should be generated."; 42 - }; 43 44 - target = lib.mkOption { 45 - type = lib.types.nonEmptyStr; 46 - example = lib.literalExpression "manifest.yaml"; 47 - description = '' 48 - Name of the symlink (relative to {file}`${manifestDir}`). 49 - Defaults to the attribute name. 50 - ''; 51 - }; 52 53 - content = lib.mkOption { 54 - type = with lib.types; nullOr (either attrs (listOf attrs)); 55 - default = null; 56 - description = '' 57 - Content of the manifest file. A single attribute set will 58 - generate a single document YAML file. A list of attribute sets 59 - will generate multiple documents separated by `---` in a single 60 - YAML file. 61 - ''; 62 }; 63 64 - source = lib.mkOption { 65 - type = lib.types.path; 66 - example = lib.literalExpression "./manifests/app.yaml"; 67 - description = '' 68 - Path of the source `.yaml` file. 69 - ''; 70 }; 71 }; 72 73 - config = { 74 - target = lib.mkDefault (mkTarget name); 75 - source = lib.mkIf (config.content != null) ( 76 - let 77 - name' = "k3s-manifest-" + builtins.baseNameOf name; 78 - docName = "k3s-manifest-doc-" + builtins.baseNameOf name; 79 - yamlDocSeparator = builtins.toFile "yaml-doc-separator" "\n---\n"; 80 - mkYaml = name: x: (pkgs.formats.yaml { }).generate name x; 81 - mkSource = 82 - value: 83 - if builtins.isList value then 84 - pkgs.concatText name' ( 85 - lib.concatMap (x: [ 86 - yamlDocSeparator 87 - (mkYaml docName x) 88 - ]) value 89 - ) 90 - else 91 - mkYaml name' value; 92 - in 93 - lib.mkDerivedConfig options.content mkSource 94 - ); 95 }; 96 - } 97 - ); 98 99 - enabledManifests = lib.filter (m: m.enable) (lib.attrValues cfg.manifests); 100 - linkManifestEntry = m: "${pkgs.coreutils-full}/bin/ln -sfn ${m.source} ${manifestDir}/${m.target}"; 101 - linkImageEntry = image: "${pkgs.coreutils-full}/bin/ln -sfn ${image} ${imageDir}/${image.name}"; 102 - linkChartEntry = 103 - let 104 - mkTarget = name: if (lib.hasSuffix ".tgz" name) then name else name + ".tgz"; 105 - in 106 - name: value: 107 - "${pkgs.coreutils-full}/bin/ln -sfn ${value} ${chartDir}/${mkTarget (builtins.baseNameOf name)}"; 108 109 - activateK3sContent = pkgs.writeShellScript "activate-k3s-content" '' 110 - ${lib.optionalString ( 111 - builtins.length enabledManifests > 0 112 - ) "${pkgs.coreutils-full}/bin/mkdir -p ${manifestDir}"} 113 - ${lib.optionalString (cfg.charts != { }) "${pkgs.coreutils-full}/bin/mkdir -p ${chartDir}"} 114 - ${lib.optionalString ( 115 - builtins.length cfg.images > 0 116 - ) "${pkgs.coreutils-full}/bin/mkdir -p ${imageDir}"} 117 118 - ${builtins.concatStringsSep "\n" (map linkManifestEntry enabledManifests)} 119 - ${builtins.concatStringsSep "\n" (lib.mapAttrsToList linkChartEntry cfg.charts)} 120 - ${builtins.concatStringsSep "\n" (map linkImageEntry cfg.images)} 121 122 - ${lib.optionalString (cfg.containerdConfigTemplate != null) '' 123 - mkdir -p $(dirname ${containerdConfigTemplateFile}) 124 - ${pkgs.coreutils-full}/bin/ln -sfn ${pkgs.writeText "config.toml.tmpl" cfg.containerdConfigTemplate} ${containerdConfigTemplateFile} 125 - ''} 126 - ''; 127 in 128 { 129 imports = [ (removeOption [ "docker" ] "k3s docker option is no longer supported.") ]; ··· 242 type = lib.types.attrsOf manifestModule; 243 default = { }; 244 example = lib.literalExpression '' 245 - deployment.source = ../manifests/deployment.yaml; 246 - my-service = { 247 - enable = false; 248 - target = "app-service.yaml"; 249 - content = { 250 - apiVersion = "v1"; 251 - kind = "Service"; 252 - metadata = { 253 - name = "app-service"; 254 - }; 255 - spec = { 256 - selector = { 257 - "app.kubernetes.io/name" = "MyApp"; 258 }; 259 - ports = [ 260 - { 261 - name = "name-of-service-port"; 262 - protocol = "TCP"; 263 - port = 80; 264 - targetPort = "http-web-svc"; 265 - } 266 - ]; 267 }; 268 - } 269 - }; 270 271 - nginx.content = [ 272 - { 273 - apiVersion = "v1"; 274 - kind = "Pod"; 275 - metadata = { 276 - name = "nginx"; 277 - labels = { 278 - "app.kubernetes.io/name" = "MyApp"; 279 }; 280 - }; 281 - spec = { 282 - containers = [ 283 - { 284 - name = "nginx"; 285 - image = "nginx:1.14.2"; 286 - ports = [ 287 - { 288 - containerPort = 80; 289 - name = "http-web-svc"; 290 - } 291 - ]; 292 - } 293 - ]; 294 - }; 295 - } 296 - { 297 - apiVersion = "v1"; 298 - kind = "Service"; 299 - metadata = { 300 - name = "nginx-service"; 301 - }; 302 - spec = { 303 - selector = { 304 - "app.kubernetes.io/name" = "MyApp"; 305 }; 306 - ports = [ 307 - { 308 - name = "name-of-service-port"; 309 - protocol = "TCP"; 310 - port = 80; 311 - targetPort = "http-web-svc"; 312 - } 313 - ]; 314 - }; 315 - } 316 - ]; 317 ''; 318 description = '' 319 Auto-deploying manifests that are linked to {file}`${manifestDir}` before k3s starts. ··· 337 Packaged Helm charts that are linked to {file}`${chartDir}` before k3s starts. 338 The attribute name will be used as the link target (relative to {file}`${chartDir}`). 339 The specified charts will only be placed on the file system and made available to the 340 - Kubernetes APIServer from within the cluster, you may use the 341 - [k3s Helm controller](https://docs.k3s.io/helm#using-the-helm-controller) 342 - to deploy the charts. This option only makes sense on server nodes 343 - (`role = server`). 344 ''; 345 }; 346 ··· 450 set the `clientConnection.kubeconfig` if you want to use `extraKubeProxyConfig`. 451 ''; 452 }; 453 }; 454 455 # implementation ··· 462 ++ (lib.optional (cfg.role != "server" && cfg.charts != { }) 463 "k3s: Helm charts are only made available to the cluster on server nodes (role == server), they will be ignored by this node." 464 ) 465 ++ (lib.optional ( 466 cfg.disableAgent && cfg.images != [ ] 467 ) "k3s: Images are only imported on nodes with an enabled agent, they will be ignored by this node") ··· 486 487 environment.systemPackages = [ config.services.k3s.package ]; 488 489 systemd.services.k3s = 490 let 491 kubeletParams = ··· 533 LimitCORE = "infinity"; 534 TasksMax = "infinity"; 535 EnvironmentFile = cfg.environmentFile; 536 - ExecStartPre = activateK3sContent; 537 ExecStart = lib.concatStringsSep " \\\n " ( 538 [ "${cfg.package}/bin/k3s ${cfg.role}" ] 539 ++ (lib.optional cfg.clusterInit "--cluster-init")
··· 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 + description = '' 194 + The hash of the packaged Helm chart. Only has an effect if `package` is not set. 195 + The Helm chart is fetched during build time and placed as a `.tgz` archive on the 196 + filesystem. 197 + ''; 198 + }; 199 + 200 + package = lib.mkOption { 201 + type = with lib.types; either path package; 202 + example = lib.literalExpression "../my-helm-chart.tgz"; 203 + description = '' 204 + The packaged Helm chart. Overwrites the options `repo`, `name`, `version` 205 + and `hash` in case of conflicts. 206 + ''; 207 + }; 208 + 209 + targetNamespace = lib.mkOption { 210 + type = lib.types.nonEmptyStr; 211 + default = "default"; 212 + example = "kube-system"; 213 + description = "The namespace in which the Helm chart gets installed."; 214 + }; 215 + 216 + createNamespace = lib.mkOption { 217 + type = lib.types.bool; 218 + default = false; 219 + example = true; 220 + description = "Whether to create the target namespace if not present."; 221 + }; 222 + 223 + values = lib.mkOption { 224 + type = with lib.types; either path attrs; 225 + default = { }; 226 + example = { 227 + replicaCount = 3; 228 + hostName = "my-host"; 229 + server = { 230 + name = "nginx"; 231 + port = 80; 232 + }; 233 }; 234 + description = '' 235 + Override default chart values via Nix expressions. This is equivalent to setting 236 + values in a `values.yaml` file. 237 238 + WARNING: The values (including secrets!) specified here are exposed unencrypted 239 + in the world-readable nix store. 240 + ''; 241 + }; 242 + 243 + extraDeploy = lib.mkOption { 244 + type = with lib.types; listOf (either path attrs); 245 + default = [ ]; 246 + example = lib.literalExpression '' 247 + [ 248 + ../manifests/my-extra-deployment.yaml 249 + { 250 + apiVersion = "v1"; 251 + kind = "Service"; 252 + metadata = { 253 + name = "app-service"; 254 + }; 255 + spec = { 256 + selector = { 257 + "app.kubernetes.io/name" = "MyApp"; 258 + }; 259 + ports = [ 260 + { 261 + name = "name-of-service-port"; 262 + protocol = "TCP"; 263 + port = 80; 264 + targetPort = "http-web-svc"; 265 + } 266 + ]; 267 + }; 268 + } 269 + ]; 270 + ''; 271 + description = "List of extra Kubernetes manifests to deploy with this Helm chart."; 272 + }; 273 + 274 + extraFieldDefinitions = lib.mkOption { 275 + inherit (yamlFormat) type; 276 + default = { }; 277 + example = { 278 + spec = { 279 + bootstrap = true; 280 + helmVersion = "v2"; 281 + backOffLimit = 3; 282 + jobImage = "custom-helm-controller:v0.0.1"; 283 + }; 284 }; 285 + description = '' 286 + Extra HelmChart field definitions that are merged with the rest of the HelmChart 287 + custom resource. This can be used to set advanced fields or to overwrite 288 + generated fields. See https://docs.k3s.io/helm#helmchart-field-definitions 289 + for possible fields. 290 + ''; 291 }; 292 + }; 293 294 + config.package = lib.mkDefault (fetchHelm { 295 + inherit (config) 296 + repo 297 + name 298 + version 299 + hash 300 + ; 301 + }); 302 + } 303 + ); 304 + 305 + manifestModule = lib.types.submodule ( 306 + { 307 + name, 308 + config, 309 + options, 310 + ... 311 + }: 312 + { 313 + options = { 314 + enable = lib.mkOption { 315 + type = lib.types.bool; 316 + default = true; 317 + description = "Whether this manifest file should be generated."; 318 }; 319 320 + target = lib.mkOption { 321 + type = lib.types.nonEmptyStr; 322 + example = "manifest.yaml"; 323 + description = '' 324 + Name of the symlink (relative to {file}`${manifestDir}`). 325 + Defaults to the attribute name. 326 + ''; 327 + }; 328 329 + content = lib.mkOption { 330 + type = with lib.types; nullOr (either attrs (listOf attrs)); 331 + default = null; 332 + description = '' 333 + Content of the manifest file. A single attribute set will 334 + generate a single document YAML file. A list of attribute sets 335 + will generate multiple documents separated by `---` in a single 336 + YAML file. 337 + ''; 338 + }; 339 340 + source = lib.mkOption { 341 + type = lib.types.path; 342 + example = lib.literalExpression "./manifests/app.yaml"; 343 + description = '' 344 + Path of the source `.yaml` file. 345 + ''; 346 + }; 347 + }; 348 349 + config = { 350 + target = lib.mkDefault (mkManifestTarget name); 351 + source = lib.mkIf (config.content != null) ( 352 + let 353 + name' = "k3s-manifest-" + builtins.baseNameOf name; 354 + docName = "k3s-manifest-doc-" + builtins.baseNameOf name; 355 + mkSource = 356 + value: 357 + if builtins.isList value then 358 + pkgs.concatText name' ( 359 + lib.concatMap (x: [ 360 + yamlDocSeparator 361 + (yamlFormat.generate docName x) 362 + ]) value 363 + ) 364 + else 365 + yamlFormat.generate name' value; 366 + in 367 + lib.mkDerivedConfig options.content mkSource 368 + ); 369 + }; 370 + } 371 + ); 372 in 373 { 374 imports = [ (removeOption [ "docker" ] "k3s docker option is no longer supported.") ]; ··· 487 type = lib.types.attrsOf manifestModule; 488 default = { }; 489 example = lib.literalExpression '' 490 + { 491 + deployment.source = ../manifests/deployment.yaml; 492 + my-service = { 493 + enable = false; 494 + target = "app-service.yaml"; 495 + content = { 496 + apiVersion = "v1"; 497 + kind = "Service"; 498 + metadata = { 499 + name = "app-service"; 500 }; 501 + spec = { 502 + selector = { 503 + "app.kubernetes.io/name" = "MyApp"; 504 + }; 505 + ports = [ 506 + { 507 + name = "name-of-service-port"; 508 + protocol = "TCP"; 509 + port = 80; 510 + targetPort = "http-web-svc"; 511 + } 512 + ]; 513 + }; 514 }; 515 + }; 516 517 + nginx.content = [ 518 + { 519 + apiVersion = "v1"; 520 + kind = "Pod"; 521 + metadata = { 522 + name = "nginx"; 523 + labels = { 524 + "app.kubernetes.io/name" = "MyApp"; 525 + }; 526 + }; 527 + spec = { 528 + containers = [ 529 + { 530 + name = "nginx"; 531 + image = "nginx:1.14.2"; 532 + ports = [ 533 + { 534 + containerPort = 80; 535 + name = "http-web-svc"; 536 + } 537 + ]; 538 + } 539 + ]; 540 + }; 541 + } 542 + { 543 + apiVersion = "v1"; 544 + kind = "Service"; 545 + metadata = { 546 + name = "nginx-service"; 547 }; 548 + spec = { 549 + selector = { 550 + "app.kubernetes.io/name" = "MyApp"; 551 + }; 552 + ports = [ 553 + { 554 + name = "name-of-service-port"; 555 + protocol = "TCP"; 556 + port = 80; 557 + targetPort = "http-web-svc"; 558 + } 559 + ]; 560 }; 561 + } 562 + ]; 563 + }; 564 ''; 565 description = '' 566 Auto-deploying manifests that are linked to {file}`${manifestDir}` before k3s starts. ··· 584 Packaged Helm charts that are linked to {file}`${chartDir}` before k3s starts. 585 The attribute name will be used as the link target (relative to {file}`${chartDir}`). 586 The specified charts will only be placed on the file system and made available to the 587 + Kubernetes APIServer from within the cluster. See the [](#opt-services.k3s.autoDeployCharts) 588 + option and the [k3s Helm controller docs](https://docs.k3s.io/helm#using-the-helm-controller) 589 + to deploy Helm charts. This option only makes sense on server nodes (`role = server`). 590 ''; 591 }; 592 ··· 696 set the `clientConnection.kubeconfig` if you want to use `extraKubeProxyConfig`. 697 ''; 698 }; 699 + 700 + autoDeployCharts = lib.mkOption { 701 + type = lib.types.attrsOf autoDeployChartsModule; 702 + apply = lib.mapAttrs mkAutoDeployChartManifest; 703 + default = { }; 704 + example = lib.literalExpression '' 705 + { 706 + harbor = { 707 + name = "harbor"; 708 + repo = "https://helm.goharbor.io"; 709 + version = "1.14.0"; 710 + hash = "sha256-fMP7q1MIbvzPGS9My91vbQ1d3OJMjwc+o8YE/BXZaYU="; 711 + values = { 712 + existingSecretAdminPassword = "harbor-admin"; 713 + expose = { 714 + tls = { 715 + enabled = true; 716 + certSource = "secret"; 717 + secret.secretName = "my-tls-secret"; 718 + }; 719 + ingress = { 720 + hosts.core = "example.com"; 721 + className = "nginx"; 722 + }; 723 + }; 724 + }; 725 + }; 726 + 727 + custom-chart = { 728 + package = ../charts/my-chart.tgz; 729 + values = ../values/my-values.yaml; 730 + extraFieldDefinitions = { 731 + spec.timeout = "60s"; 732 + }; 733 + }; 734 + } 735 + ''; 736 + description = '' 737 + Auto deploying Helm charts that are installed by the k3s Helm controller. Avoid to use 738 + attribute names that are also used in the [](#opt-services.k3s.manifests) and 739 + [](#opt-services.k3s.charts) options. Manifests with the same name will override 740 + auto deploying charts with the same name. Similiarly, charts with the same name will 741 + overwrite the Helm chart contained in auto deploying charts. This option only makes 742 + sense on server nodes (`role = server`). See the 743 + [k3s Helm documentation](https://docs.k3s.io/helm) for further information. 744 + ''; 745 + }; 746 }; 747 748 # implementation ··· 755 ++ (lib.optional (cfg.role != "server" && cfg.charts != { }) 756 "k3s: Helm charts are only made available to the cluster on server nodes (role == server), they will be ignored by this node." 757 ) 758 + ++ (lib.optional (cfg.role != "server" && cfg.autoDeployCharts != { }) 759 + "k3s: Auto deploying Helm charts are only installed on server nodes (role == server), they will be ignored by this node." 760 + ) 761 + ++ (lib.optional (duplicateManifests != [ ]) 762 + "k3s: The following auto deploying charts are overriden by manifests of the same name: ${toString duplicateManifests}." 763 + ) 764 + ++ (lib.optional (duplicateCharts != [ ]) 765 + "k3s: The following auto deploying charts are overriden by charts of the same name: ${toString duplicateCharts}." 766 + ) 767 ++ (lib.optional ( 768 cfg.disableAgent && cfg.images != [ ] 769 ) "k3s: Images are only imported on nodes with an enabled agent, they will be ignored by this node") ··· 788 789 environment.systemPackages = [ config.services.k3s.package ]; 790 791 + # Use systemd-tmpfiles to activate k3s content 792 + systemd.tmpfiles.settings."10-k3s" = 793 + let 794 + # Merge manifest with manifests generated from auto deploying charts, keep only enabled manifests 795 + enabledManifests = lib.filterAttrs (_: v: v.enable) (cfg.autoDeployCharts // cfg.manifests); 796 + # Merge charts with charts contained in enabled auto deploying charts 797 + helmCharts = 798 + (lib.concatMapAttrs (n: v: { ${n} = v.package; }) ( 799 + lib.filterAttrs (_: v: v.enable) cfg.autoDeployCharts 800 + )) 801 + // cfg.charts; 802 + # Make a systemd-tmpfiles rule for a manifest 803 + mkManifestRule = manifest: { 804 + name = "${manifestDir}/${manifest.target}"; 805 + value = { 806 + "L+".argument = "${manifest.source}"; 807 + }; 808 + }; 809 + # Ensure that all chart targets have a .tgz suffix 810 + mkChartTarget = name: if (lib.hasSuffix ".tgz" name) then name else name + ".tgz"; 811 + # Make a systemd-tmpfiles rule for a chart 812 + mkChartRule = target: source: { 813 + name = "${chartDir}/${mkChartTarget target}"; 814 + value = { 815 + "L+".argument = "${source}"; 816 + }; 817 + }; 818 + # Make a systemd-tmpfiles rule for a container image 819 + mkImageRule = image: { 820 + name = "${imageDir}/${image.name}"; 821 + value = { 822 + "L+".argument = "${image}"; 823 + }; 824 + }; 825 + in 826 + (lib.mapAttrs' (_: v: mkManifestRule v) enabledManifests) 827 + // (lib.mapAttrs' (n: v: mkChartRule n v) helmCharts) 828 + // (builtins.listToAttrs (map mkImageRule cfg.images)) 829 + // (lib.optionalAttrs (cfg.containerdConfigTemplate != null) { 830 + ${containerdConfigTemplateFile} = { 831 + "L+".argument = "${pkgs.writeText "config.toml.tmpl" cfg.containerdConfigTemplate}"; 832 + }; 833 + }); 834 + 835 systemd.services.k3s = 836 let 837 kubeletParams = ··· 879 LimitCORE = "infinity"; 880 TasksMax = "infinity"; 881 EnvironmentFile = cfg.environmentFile; 882 ExecStart = lib.concatStringsSep " \\\n " ( 883 [ "${cfg.package}/bin/k3s ${cfg.role}" ] 884 ++ (lib.optional cfg.clusterInit "--cluster-init")
+135
nixos/tests/k3s/auto-deploy-charts.nix
···
··· 1 + # Tests whether container images are imported and auto deploying Helm charts work 2 + import ../make-test-python.nix ( 3 + { 4 + k3s, 5 + lib, 6 + pkgs, 7 + ... 8 + }: 9 + let 10 + testImageEnv = pkgs.buildEnv { 11 + name = "k3s-pause-image-env"; 12 + paths = with pkgs; [ 13 + busybox 14 + hello 15 + ]; 16 + }; 17 + testImage = pkgs.dockerTools.buildImage { 18 + name = "test.local/test"; 19 + tag = "local"; 20 + # Slightly reduces the time needed to import image 21 + compressor = "zstd"; 22 + copyToRoot = testImageEnv; 23 + }; 24 + # pack the test helm chart as a .tgz archive 25 + package = 26 + pkgs.runCommand "k3s-test-chart.tgz" 27 + { 28 + nativeBuildInputs = [ pkgs.kubernetes-helm ]; 29 + } 30 + '' 31 + helm package ${./k3s-test-chart} 32 + mv ./*.tgz $out 33 + ''; 34 + # The common Helm chart that is used in this test 35 + testChart = { 36 + inherit package; 37 + values = { 38 + runCommand = "hello"; 39 + image = { 40 + repository = testImage.imageName; 41 + tag = testImage.imageTag; 42 + }; 43 + }; 44 + }; 45 + in 46 + { 47 + name = "${k3s.name}-auto-deploy-helm"; 48 + meta.maintainers = lib.teams.k3s.members; 49 + nodes.machine = 50 + { pkgs, ... }: 51 + { 52 + # k3s uses enough resources the default vm fails. 53 + virtualisation = { 54 + memorySize = 1536; 55 + diskSize = 4096; 56 + }; 57 + environment.systemPackages = [ pkgs.yq-go ]; 58 + services.k3s = { 59 + enable = true; 60 + package = k3s; 61 + # Slightly reduce resource usage 62 + extraFlags = [ 63 + "--disable coredns" 64 + "--disable local-storage" 65 + "--disable metrics-server" 66 + "--disable servicelb" 67 + "--disable traefik" 68 + ]; 69 + images = [ 70 + # Provides the k3s Helm controller 71 + k3s.airgapImages 72 + testImage 73 + ]; 74 + autoDeployCharts = { 75 + # regular test chart that should get installed 76 + hello = testChart; 77 + # disabled chart that should not get installed 78 + disabled = testChart // { 79 + enable = false; 80 + }; 81 + # advanced chart that should get installed in the "test" namespace with a custom 82 + # timeout and overridden values 83 + advanced = testChart // { 84 + # create the "test" namespace via extraDeploy for testing 85 + extraDeploy = [ 86 + { 87 + apiVersion = "v1"; 88 + kind = "Namespace"; 89 + metadata.name = "test"; 90 + } 91 + ]; 92 + extraFieldDefinitions = { 93 + spec = { 94 + # overwrite chart values 95 + valuesContent = '' 96 + runCommand: "echo 'advanced hello'" 97 + image: 98 + repository: ${testImage.imageName} 99 + tag: ${testImage.imageTag} 100 + ''; 101 + # overwrite the chart namespace 102 + targetNamespace = "test"; 103 + # set a custom timeout 104 + timeout = "69s"; 105 + }; 106 + }; 107 + }; 108 + }; 109 + }; 110 + }; 111 + 112 + testScript = # python 113 + '' 114 + import json 115 + 116 + machine.wait_for_unit("k3s") 117 + # check existence/absence of chart manifest files 118 + machine.succeed("test -e /var/lib/rancher/k3s/server/manifests/hello.yaml") 119 + machine.succeed("test ! -e /var/lib/rancher/k3s/server/manifests/disabled.yaml") 120 + machine.succeed("test -e /var/lib/rancher/k3s/server/manifests/advanced.yaml") 121 + # check that the timeout is set correctly, select only the first doc in advanced.yaml 122 + advancedManifest = json.loads(machine.succeed("yq -o json 'select(di == 0)' /var/lib/rancher/k3s/server/manifests/advanced.yaml")) 123 + assert advancedManifest["spec"]["timeout"] == "69s", f"unexpected value for spec.timeout: {advancedManifest["spec"]["timeout"]}" 124 + # wait for test jobs to complete 125 + machine.wait_until_succeeds("kubectl wait --for=condition=complete job/hello", timeout=180) 126 + machine.wait_until_succeeds("kubectl -n test wait --for=condition=complete job/advanced", timeout=180) 127 + # check output of test jobs 128 + hello_output = machine.succeed("kubectl logs -l batch.kubernetes.io/job-name=hello") 129 + advanced_output = machine.succeed("kubectl -n test logs -l batch.kubernetes.io/job-name=advanced") 130 + # strip the output to remove trailing whitespaces 131 + assert hello_output.rstrip() == "Hello, world!", f"unexpected output of hello job: {hello_output}" 132 + assert advanced_output.rstrip() == "advanced hello", f"unexpected output of advanced job: {advanced_output}" 133 + ''; 134 + } 135 + )
+3
nixos/tests/k3s/default.nix
··· 11 _: k3s: import ./airgap-images.nix { inherit system pkgs k3s; } 12 ) allK3s; 13 auto-deploy = lib.mapAttrs (_: k3s: import ./auto-deploy.nix { inherit system pkgs k3s; }) allK3s; 14 containerd-config = lib.mapAttrs ( 15 _: k3s: import ./containerd-config.nix { inherit system pkgs k3s; } 16 ) allK3s;
··· 11 _: k3s: import ./airgap-images.nix { inherit system pkgs k3s; } 12 ) allK3s; 13 auto-deploy = lib.mapAttrs (_: k3s: import ./auto-deploy.nix { inherit system pkgs k3s; }) allK3s; 14 + auto-deploy-charts = lib.mapAttrs ( 15 + _: k3s: import ./auto-deploy-charts.nix { inherit system pkgs k3s; } 16 + ) allK3s; 17 containerd-config = lib.mapAttrs ( 18 _: k3s: import ./containerd-config.nix { inherit system pkgs k3s; } 19 ) allK3s;
+24
nixos/tests/k3s/k3s-test-chart/Chart.yaml
···
··· 1 + apiVersion: v2 2 + name: k3s-test-chart 3 + description: A Helm chart that is used in k3s NixOS tests. 4 + 5 + # A chart can be either an 'application' or a 'library' chart. 6 + # 7 + # Application charts are a collection of templates that can be packaged into versioned archives 8 + # to be deployed. 9 + # 10 + # Library charts provide useful utilities or functions for the chart developer. They're included as 11 + # a dependency of application charts to inject those utilities and functions into the rendering 12 + # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 + type: application 14 + 15 + # This is the chart version. This version number should be incremented each time you make changes 16 + # to the chart and its templates, including the app version. 17 + # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 + version: 0.1.0 19 + 20 + # This is the version number of the application being deployed. This version number should be 21 + # incremented each time you make changes to the application. Versions are not expected to 22 + # follow Semantic Versioning. They should reflect the version the application is using. 23 + # It is recommended to use it with quotes. 24 + appVersion: "1.16.0"
+14
nixos/tests/k3s/k3s-test-chart/templates/job.yaml
···
··· 1 + apiVersion: batch/v1 2 + kind: Job 3 + metadata: 4 + name: {{ .Release.Name | quote }} 5 + namespace: {{ .Release.Namespace | quote }} 6 + spec: 7 + template: 8 + spec: 9 + containers: 10 + - name: test 11 + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 12 + command: ["sh"] 13 + args: ["-c", "{{ .Values.runCommand }}"] 14 + restartPolicy: {{ .Values.restartPolicy | quote }}
+5
nixos/tests/k3s/k3s-test-chart/values.yaml
···
··· 1 + restartPolicy: "Never" 2 + runCommand: "" 3 + image: 4 + repository: foo 5 + tag: 1.0.0