diff --git a/default.nix b/default.nix index 7ba1e46..3b4f264 100644 --- a/default.nix +++ b/default.nix @@ -29,5 +29,6 @@ let k8s-submodules = ./k8s/submodule.nix; istio = ./istio; testing = ./testing; + helm = ./helm; }; in kubenix diff --git a/helm/chart2json.nix b/helm/chart2json.nix new file mode 100644 index 0000000..a126e8c --- /dev/null +++ b/helm/chart2json.nix @@ -0,0 +1,51 @@ +{ stdenvNoCC, lib, kubernetes-helm, gawk, remarshal, jq }: + +with lib; + +{ + # chart to template + chart + + # release name +, name + + # namespace to install release into +, namespace ? null + + # values to pass to chart +, values ? {} + + # kubernetes version to template chart for +, kubeVersion ? null }: let + valuesJsonFile = builtins.toFile "${name}-values.json" (builtins.toJSON values); +in stdenvNoCC.mkDerivation { + name = "${name}.json"; + buildCommand = '' + # template helm file and write resources to yaml + helm template --name "${name}" \ + ${optionalString (kubeVersion != null) "--kube-version ${kubeVersion}"} \ + ${optionalString (namespace != null) "--namespace ${namespace}"} \ + ${optionalString (values != {}) "-f ${valuesJsonFile}"} \ + ${chart} >resources.yaml + + # split multy yaml file into multiple files + awk 'BEGIN{i=1}{line[i++]=$0}END{j=1;n=0; while (j>"resource-"n".yaml"; j++}}' resources.yaml + + # join multiple yaml files in jsonl file + for file in ./resource-*.yaml + do + remarshal -i $file -if yaml -of json >>resources.jsonl + done + + # convert jsonl file to json array, remove null values and write to $out + cat resources.jsonl | jq -Scs 'walk( + if type == "object" then + with_entries(select(.value != null)) + elif type == "array" then + map(select(. != null)) + else + . + end)' > $out + ''; + nativeBuildInputs = [ kubernetes-helm gawk remarshal jq ]; +} diff --git a/helm/default.nix b/helm/default.nix new file mode 100644 index 0000000..065272c --- /dev/null +++ b/helm/default.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, kubenix, ... }: + +with lib; + +let + cfg = config.kubernetes.helm; + + globalConfig = config; + + recursiveAttrs = mkOptionType { + name = "recursive-attrs"; + description = "recursive attribute set"; + check = isAttrs; + merge = loc: foldl' (res: def: recursiveUpdate res def.value) {}; + }; + + parseApiVersion = apiVersion: let + splitted = splitString "/" apiVersion; + in { + group = if length splitted == 1 then "core" else head splitted; + version = last splitted; + }; + + chart2json = pkgs.callPackage ./chart2json.nix { }; + fetchhelm = pkgs.callPackage ./fetchhelm.nix { }; + +in { + imports = [ + kubenix.k8s + ]; + + options.kubernetes.helm = { + instances = mkOption { + description = "Attribute set of helm instances"; + type = types.attrsOf (types.submodule ({ config, name, ... }: { + options = { + name = mkOption { + description = "Helm release name"; + type = types.str; + default = name; + }; + + chart = mkOption { + description = "Helm chart to use"; + type = types.package; + }; + + namespace = mkOption { + description = "Namespace to install helm chart to"; + type = types.nullOr types.str; + default = null; + }; + + values = mkOption { + description = "Values to pass to chart"; + type = recursiveAttrs; + default = {}; + }; + + kubeVersion = mkOption { + description = "Kubernetes version to build chart for"; + type = types.str; + default = globalConfig.kubernetes.version; + }; + + overrides = mkOption { + description = "Overrides to apply to all chart resources"; + type = types.listOf types.unspecified; + default = []; + }; + + overrideNamespace = mkOption { + description = "Whether to apply namespace override"; + type = types.bool; + default = true; + }; + + objects = mkOption { + description = "Generated kubernetes objects"; + type = types.listOf types.attrs; + default = []; + }; + }; + + config.overrides = mkIf (config.overrideNamespace && config.namespace != null) [{ + metadata.namespace = mkDefault config.namespace; + }]; + + config.objects = importJSON (chart2json { + inherit (config) chart name namespace values kubeVersion; + }); + })); + }; + }; + + # include helper helm methods as args + config._module.args.helm = { + fetch = fetchhelm; + chart2json = chart2json; + }; + + config.kubernetes.api = mkMerge (flatten (mapAttrsToList (_: instance: + map (object: let + apiVersion = parseApiVersion object.apiVersion; + name = object.metadata.name; + in { + "${apiVersion.group}"."${apiVersion.version}".${object.kind}."${name}" = mkMerge ([ + object + ] ++ instance.overrides); + }) instance.objects + ) cfg.instances)); +} diff --git a/helm/fetchhelm.nix b/helm/fetchhelm.nix new file mode 100644 index 0000000..b6d9069 --- /dev/null +++ b/helm/fetchhelm.nix @@ -0,0 +1,48 @@ +{ stdenvNoCC, lib, kubernetes-helm, cacert }: + +let + cleanName = name: lib.replaceStrings ["/"] ["-"] name; + +in { + # name of the chart + chart + + # chart url to fetch from custom location +, chartUrl ? null + + # version of the chart +, version ? null + +# chart hash +, sha256 + +# whether to extract chart +, untar ? true + +# use custom charts repo +, repo ? null + +# pass --verify to helm chart +, verify ? false + +# pass --devel to helm chart +, devel ? false }: stdenvNoCC.mkDerivation { + name = "${cleanName chart}-${if version == null then "dev" else version}"; + + buildCommand = '' + export HOME="$PWD" + helm init --client-only >/dev/null + ${if repo == null then "" else "helm repo add repository ${repo}"} + helm fetch -d ./chart \ + ${if untar then "--untar" else ""} \ + ${if version == null then "" else "--version ${version}"} \ + ${if devel then "--devel" else ""} \ + ${if verify then "--verify" else ""} \ + ${if chartUrl == null then (if repo == null then chart else "repository/${chart}") else chartUrl} + cp -r chart/*/ $out + ''; + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + outputHash = sha256; + nativeBuildInputs = [ kubernetes-helm cacert ]; +} diff --git a/helm/test.nix b/helm/test.nix new file mode 100644 index 0000000..84c7754 --- /dev/null +++ b/helm/test.nix @@ -0,0 +1,43 @@ +{ pkgs ? import {} }: + +let + fetchhelm = pkgs.callPackage ./fetchhelm.nix { }; + chart2json = pkgs.callPackage ./chart2json.nix { }; +in rec { + postgresql-chart = fetchhelm { + chart = "stable/postgresql"; + version = "0.18.1"; + sha256 = "1p3gfmaakxrqb4ncj6nclyfr5afv7xvcdw95c6qyazfg72h3zwjn"; + }; + + istio-chart = fetchhelm { + chart = "istio"; + version = "1.1.0"; + repo = "https://storage.googleapis.com/istio-release/releases/1.1.0-rc.0/charts"; + sha256 = "0ippv2914hwpsb3kkhk8d839dii5whgrhxjwhpb9vdwgji5s7yfl"; + }; + + istio-official-chart = pkgs.fetchgit { + url = "https://github.com/fyery-chen/istio-helm"; + rev = "47e235e775314daeb88a3a53689ed66c396ecd3f"; + sha256 = "190sfyvhdskw6ijy8cprp6hxaazn7s7mg5ids4snshk1pfdg2q8h"; + }; + + postgresql-json = chart2json { + name = "postgresql"; + chart = postgresql-chart; + values = { + networkPolicy.enabled = true; + }; + }; + + istio-json = chart2json { + name = "istio"; + chart = istio-chart; + }; + + istio-official-json = chart2json { + name = "istio-official"; + chart = "${istio-official-chart}/istio-official"; + }; +} diff --git a/tests/default.nix b/tests/default.nix index ff72151..0d02a8b 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -32,6 +32,7 @@ let ./k8s/1.13/crd.nix ./k8s/submodule.nix ./k8s/defaults.nix + ./helm/simple.nix ./istio/bookinfo.nix ./submodules/simple.nix ./submodules/defaults.nix diff --git a/tests/helm/simple.nix b/tests/helm/simple.nix new file mode 100644 index 0000000..08a4c5d --- /dev/null +++ b/tests/helm/simple.nix @@ -0,0 +1,40 @@ +{ config, test, kubenix, k8s, helm, ... }: + +with k8s; + +let + corev1 = config.kubernetes.api.core.v1; + appsv1beta2 = config.kubernetes.api.apps.v1beta2; +in { + imports = [ + kubenix.helm + ]; + + test = { + name = "helm-simple"; + description = "Simple k8s testing wheter name, apiVersion and kind are preset"; + assertions = [{ + message = "should have generated resources"; + assertion = + appsv1beta2.StatefulSet ? "app-psql-postgreql" && + corev1.ConfigMap ? "app-psql-postgresql-init-scripts" && + corev1.Secret ? "app-psql-postgresql" && + corev1.Service ? "app-psql-postgresql-headless" ; + } { + message = "should have namespace defined"; + assertion = + appsv1beta2.StatefulSet.app-psql-postgresql.metadata.namespace == "test-namespace"; + }]; + }; + + kubernetes.api.namespaces.test-namespace = {}; + + kubernetes.helm.instances.app-psql = { + namespace = "test-namespace"; + chart = helm.fetch { + chart = "stable/postgresql"; + version = "3.0.0"; + sha256 = "0icnnpcqvf1hqn7fc9niyifd0amlm9jfrx3iks0y360rk8wndbch"; + }; + }; +}