├── .gitattributes ├── .gitignore ├── .envrc ├── jobs ├── default.nix └── generators │ ├── default.nix │ ├── istio │ └── default.nix │ └── k8s │ └── default.nix ├── lib ├── helm │ ├── default.nix │ ├── test.nix │ ├── fetchhelm.nix │ └── chart2json.nix ├── default.nix ├── docker │ └── default.nix ├── upstreamables.nix └── k8s │ └── default.nix ├── shell.nix ├── default.nix ├── .editorconfig ├── tests ├── k8s │ ├── pod.json │ ├── deployment.yaml │ ├── simple.nix │ ├── imports.nix │ ├── defaults.nix │ ├── submodule.nix │ ├── order.nix │ ├── crd.nix │ └── deployment.nix ├── submodules │ ├── exports.nix │ ├── passthru.nix │ ├── simple.nix │ ├── versioning.nix │ └── defaults.nix ├── default.nix ├── images.nix ├── istio │ └── bookinfo.nix └── helm │ └── simple.nix ├── compat.nix ├── examples ├── default.nix └── nginx-deployment │ ├── image.nix │ ├── README.md │ ├── test.nix │ ├── default.nix │ └── module.nix ├── modules ├── default.nix ├── testing │ ├── driver │ │ ├── kubetestdrv.nix │ │ └── kubetest.nix │ ├── runtime │ │ ├── local.nix │ │ └── nixos-k8s.nix │ ├── docker.nix │ ├── test-options.nix │ ├── default.nix │ └── evalTest.nix ├── istio-overrides.nix ├── base.nix ├── submodule.nix ├── docker.nix ├── helm.nix ├── submodules.nix └── k8s.nix ├── .github └── workflows │ └── ci.yml ├── devshell.toml ├── pkgs └── applications │ └── networking │ └── cluster │ ├── kubectl │ └── default.nix │ └── kubernetes │ ├── fixup-addonmanager-lib-path.patch │ ├── default.nix │ └── mk-docker-opts.sh ├── LICENSE ├── README.md ├── flake.lock └── flake.nix /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result* 2 | shared/* 3 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | watch_file devshell.toml flake.nix 2 | use flake || use nix 3 | -------------------------------------------------------------------------------- /jobs/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | { 3 | generators = pkgs.callPackage ./generators { }; 4 | } 5 | -------------------------------------------------------------------------------- /lib/helm/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | { 4 | chart2json = pkgs.callPackage ./chart2json.nix { }; 5 | fetch = pkgs.callPackage ./fetchhelm.nix { }; 6 | } 7 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem }: 2 | let 3 | in 4 | ( 5 | (import ./compat.nix).flake-compat { 6 | src = ./.; 7 | inherit system; 8 | } 9 | ).shellNix 10 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem }: 2 | let 3 | in 4 | ( 5 | (import ./compat.nix).flake-compat { 6 | src = ./.; 7 | inherit system; 8 | } 9 | ).defaultNix 10 | -------------------------------------------------------------------------------- /lib/default.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs }: 2 | 3 | { 4 | k8s = import ./k8s { inherit lib; }; 5 | docker = import ./docker { inherit lib pkgs; }; 6 | helm = import ./helm { inherit pkgs; }; 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /tests/k8s/pod.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "Pod", 3 | "apiVersion": "v1", 4 | "metadata": { 5 | "name": "test" 6 | }, 7 | "spec": { 8 | "containers": [{ 9 | "name": "test", 10 | "image": "busybox" 11 | }] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /compat.nix: -------------------------------------------------------------------------------- 1 | { 2 | flake-compat = import (builtins.fetchurl { 3 | url = "https://raw.githubusercontent.com/edolstra/flake-compat/99f1c2157fba4bfe6211a321fd0ee43199025dbf/default.nix"; 4 | sha256 = "1vas5z58901gavy5d53n1ima482yvly405jp9l8g07nr4abmzsyb"; 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /examples/default.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem 2 | , evalModules ? (import ../. { }).evalModules.${system} 3 | }: 4 | 5 | { registry ? "docker.io/gatehub" }: 6 | 7 | { 8 | nginx-deployment = import ./nginx-deployment { inherit evalModules registry; }; 9 | } 10 | -------------------------------------------------------------------------------- /modules/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | k8s = ./k8s.nix; 3 | istio = ./istio.nix; 4 | submodules = ./submodules.nix; 5 | submodule = ./submodule.nix; 6 | helm = ./helm.nix; 7 | docker = ./docker.nix; 8 | testing = ./testing; 9 | test = ./testing/test-options.nix; 10 | base = ./base.nix; 11 | } 12 | -------------------------------------------------------------------------------- /tests/k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:1.7.9 20 | ports: 21 | - containerPort: 80 22 | -------------------------------------------------------------------------------- /modules/testing/driver/kubetestdrv.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | with pkgs; 3 | with pkgs.python38Packages; 4 | 5 | with pkgs.python38; 6 | pkgs.python38Packages.buildPythonPackage rec { 7 | pname = "kubetest"; 8 | version = "0.9.5"; 9 | src = fetchPypi { 10 | inherit pname version; 11 | sha256 = "sha256-TqDHMciAEXv4vMWLJY1YdtXsP4ho+INgdFB3xQQNoZU="; 12 | }; 13 | propagatedBuildInputs = [ pytest kubernetes ]; 14 | doCheck = false; 15 | } 16 | -------------------------------------------------------------------------------- /examples/nginx-deployment/image.nix: -------------------------------------------------------------------------------- 1 | { dockerTools, nginx }: 2 | 3 | dockerTools.buildLayeredImage { 4 | name = "nginx"; 5 | contents = [ nginx ]; 6 | extraCommands = '' 7 | mkdir -p etc 8 | chmod u+w etc 9 | echo "nginx:x:1000:1000::/:" > etc/passwd 10 | echo "nginx:x:1000:nginx" > etc/group 11 | ''; 12 | config = { 13 | Cmd = [ "nginx" "-c" "/etc/nginx/nginx.conf" ]; 14 | ExposedPorts = { 15 | "80/tcp" = { }; 16 | }; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /lib/docker/default.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs }: 2 | 3 | with lib; 4 | 5 | { 6 | copyDockerImages = { images, dest, args ? "" }: 7 | pkgs.writeScript "copy-docker-images.sh" (concatMapStrings 8 | (image: '' 9 | #!${pkgs.runtimeShell} 10 | 11 | set -e 12 | 13 | echo "copying '${image.imageName}:${image.imageTag}' to '${dest}/${image.imageName}:${image.imageTag}'" 14 | ${pkgs.skopeo}/bin/skopeo copy ${args} $@ docker-archive:${image} ${dest}/${image.imageName}:${image.imageTag} 15 | '') 16 | images); 17 | } 18 | -------------------------------------------------------------------------------- /modules/istio-overrides.nix: -------------------------------------------------------------------------------- 1 | { lib, definitions }: 2 | 3 | with lib; 4 | 5 | { 6 | "istio_networking_v1alpha3_StringMatch" = recursiveUpdate 7 | (recursiveUpdate 8 | definitions."istio_networking_v1alpha3_StringMatch_Exact" 9 | definitions."istio_networking_v1alpha3_StringMatch_Prefix" 10 | ) 11 | definitions."istio_networking_v1alpha3_StringMatch_Regex"; 12 | 13 | "istio_networking_v1alpha3_PortSelector" = recursiveUpdate 14 | definitions."istio_networking_v1alpha3_PortSelector_Name" 15 | definitions."istio_networking_v1alpha3_PortSelector_Number"; 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2.3.4 10 | - uses: cachix/install-nix-action@v13 11 | with: 12 | install_url: https://github.com/numtide/nix-flakes-installer/releases/download/nix-2.4pre20210429_d15a196/install 13 | extra_nix_config: experimental-features = nix-command flakes 14 | 15 | - name: Run Nix Flake Check 16 | run: nix -Lv flake check 17 | 18 | - name: Check Nix parsing 19 | run: nix -Lv develop -c "evalnix" 20 | -------------------------------------------------------------------------------- /examples/nginx-deployment/README.md: -------------------------------------------------------------------------------- 1 | # Example: kubernetes nginx deployment 2 | 3 | A simple example creating kubernetes nginx deployment and associated docker 4 | image 5 | 6 | ## Usage 7 | 8 | ### Building and applying kubernetes configuration 9 | 10 | ``` 11 | nix eval -f ./. --json result | kubectl apply -f - 12 | ``` 13 | 14 | ### Building and pushing docker images 15 | 16 | ``` 17 | nix run -f ./. pushDockerImages -c copy-docker-images 18 | ``` 19 | 20 | ### Running tests 21 | 22 | Test will spawn vm with kubernetes and run test script, which checks if everyting 23 | works as expected. 24 | 25 | ``` 26 | nix build -f ./. test-script 27 | cat result | jq '.' 28 | ``` 29 | -------------------------------------------------------------------------------- /tests/k8s/simple.nix: -------------------------------------------------------------------------------- 1 | { config, kubenix, ... }: 2 | let 3 | cfg = config.kubernetes.api.resources.pods.nginx; 4 | in 5 | { 6 | imports = [ kubenix.modules.test kubenix.modules.k8s ]; 7 | 8 | test = { 9 | name = "k8s-simple"; 10 | description = "Simple k8s testing wheter name, apiVersion and kind are preset"; 11 | assertions = [{ 12 | message = "should have apiVersion and kind set"; 13 | assertion = cfg.apiVersion == "v1" && cfg.kind == "Pod"; 14 | } 15 | { 16 | message = "should have name set"; 17 | assertion = cfg.metadata.name == "nginx"; 18 | }]; 19 | }; 20 | 21 | kubernetes.resources.pods.nginx = { }; 22 | } 23 | -------------------------------------------------------------------------------- /tests/submodules/exports.nix: -------------------------------------------------------------------------------- 1 | { name, config, lib, kubenix, subm-lib, ... }: 2 | 3 | with lib; 4 | let 5 | submodule = { 6 | imports = [ kubenix.modules.submodule ]; 7 | 8 | config.submodule = { 9 | name = "subm"; 10 | exports = { 11 | inherit id; 12 | }; 13 | }; 14 | }; 15 | in 16 | { 17 | imports = with kubenix.modules; [ test submodules ]; 18 | 19 | test = { 20 | name = "submodules-exports"; 21 | description = "Submodules exports test"; 22 | assertions = [{ 23 | message = "should have library exported"; 24 | assertion = subm-lib.id 1 == 1; 25 | }]; 26 | }; 27 | 28 | submodules.imports = [{ 29 | modules = [ submodule ]; 30 | exportAs = "subm-lib"; 31 | }]; 32 | } 33 | -------------------------------------------------------------------------------- /tests/k8s/imports.nix: -------------------------------------------------------------------------------- 1 | { config, lib, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | pod = config.kubernetes.api.resources.core.v1.Pod.test; 6 | deployment = config.kubernetes.api.resources.apps.v1.Deployment.nginx-deployment; 7 | in 8 | { 9 | imports = with kubenix.modules; [ test k8s ]; 10 | 11 | test = { 12 | name = "k8s-imports"; 13 | description = "Simple k8s testing imports"; 14 | enable = builtins.compareVersions config.kubernetes.version "1.10" >= 0; 15 | assertions = [{ 16 | message = "Pod should have name set"; 17 | assertion = pod.metadata.name == "test"; 18 | } 19 | { 20 | message = "Deployment should have name set"; 21 | assertion = deployment.metadata.name == "nginx-deployment"; 22 | }]; 23 | }; 24 | 25 | kubernetes.imports = [ 26 | ./pod.json 27 | ./deployment.yaml 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /devshell.toml: -------------------------------------------------------------------------------- 1 | [[env]] 2 | name = "QEMU_NET_OPTS" 3 | value = "hostfwd=tcp::5443-:443" 4 | 5 | [[env]] 6 | name = "KUBECONFIG" 7 | eval = "$DEVSHELL_ROOT/kubeconfig.json" 8 | 9 | [devshell] 10 | name = "kubenix" 11 | packages = [ 12 | "fd", 13 | "nixpkgs-fmt", 14 | "dive", 15 | "kube3d", 16 | "kubie", 17 | "k9s", 18 | ] 19 | 20 | [[commands]] 21 | name = "fmt" 22 | help = "Check Nix formatting" 23 | category = "checks" 24 | command = "nixpkgs-fmt ${@} ." 25 | 26 | [[commands]] 27 | name = "evalnix" 28 | help = "Check Nix parsing" 29 | category = "checks" 30 | command = "fd --extension nix --exec nix-instantiate --parse --quiet {} >/dev/null" 31 | 32 | # K8s related tools 33 | [[commands]] 34 | package = "dive" 35 | category = "k8s" 36 | 37 | [[commands]] 38 | package = "kubie" 39 | category = "k8s" 40 | 41 | [[commands]] 42 | package = "k9s" 43 | category = "k8s" 44 | 45 | -------------------------------------------------------------------------------- /pkgs/applications/networking/cluster/kubectl/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv, kubernetes, installShellFiles }: 2 | 3 | stdenv.mkDerivation { 4 | name = "kubectl-${kubernetes.version}"; 5 | 6 | # kubectl is currently part of the main distribution but will eventially be 7 | # split out (see homepage) 8 | dontUnpack = true; 9 | 10 | nativeBuildInputs = [ installShellFiles ]; 11 | 12 | outputs = [ "out" "man" ]; 13 | 14 | installPhase = '' 15 | install -D ${kubernetes}/bin/kubectl -t $out/bin 16 | 17 | installManPage "${kubernetes.man}/share/man/man1"/kubectl* 18 | 19 | for shell in bash zsh; do 20 | $out/bin/kubectl completion $shell > kubectl.$shell 21 | installShellCompletion kubectl.$shell 22 | done 23 | ''; 24 | 25 | meta = kubernetes.meta // { 26 | description = "Kubernetes CLI"; 27 | homepage = "https://github.com/kubernetes/kubectl"; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /modules/testing/runtime/local.nix: -------------------------------------------------------------------------------- 1 | { lib, config, pkgs, ... }: 2 | 3 | with lib; 4 | let 5 | testing = config.testing; 6 | 7 | script = pkgs.writeScript "run-local-k8s-tests-${testing.name}.sh" '' 8 | #!${pkgs.runtimeShell} 9 | 10 | set -e 11 | 12 | KUBECONFIG=''${KUBECONFIG:-~/.kube/config} 13 | SKOPEOARGS="" 14 | 15 | while (( "$#" )); do 16 | case "$1" in 17 | --kubeconfig) 18 | KUBECONFIG=$2 19 | shift 2 20 | ;; 21 | --skopeo-args) 22 | SKOPEOARGS=$2 23 | shift 2 24 | ;; 25 | esac 26 | done 27 | 28 | echo "--> copying docker images to registry" 29 | ${testing.docker.copyScript} $SKOPEOARGS 30 | 31 | echo "--> running tests" 32 | ${testing.testScript} --kube-config=$KUBECONFIG 33 | ''; 34 | in 35 | { 36 | options.testing.runtime.local = { 37 | script = mkOption { 38 | type = types.package; 39 | description = "Runtime script"; 40 | }; 41 | }; 42 | 43 | config.testing.runtime.local.script = script; 44 | } 45 | -------------------------------------------------------------------------------- /examples/nginx-deployment/test.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, kubenix, test, ... }: 2 | 3 | with lib; 4 | 5 | { 6 | imports = [ kubenix.modules.test ./module.nix ]; 7 | 8 | test = { 9 | name = "nginx-deployment"; 10 | description = "Test testing nginx deployment"; 11 | script = '' 12 | @pytest.mark.applymanifest('${test.kubernetes.resultYAML}') 13 | def test_nginx_deployment(kube): 14 | """Tests whether nginx deployment gets successfully created""" 15 | 16 | kube.wait_for_registered(timeout=30) 17 | 18 | deployments = kube.get_deployments() 19 | nginx_deploy = deployments.get('nginx') 20 | assert nginx_deploy is not None 21 | 22 | status = nginx_deploy.status() 23 | assert status.readyReplicas == 10 24 | 25 | # TODO: implement those kind of checks from the host machine into the cluster 26 | # via port forwarding, prepare all runtimes accordingly 27 | # ${pkgs.curl}/bin/curl http://nginx.default.svc.cluster.local | grep -i hello 28 | ''; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /modules/base.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | with lib; 4 | 5 | { 6 | options = { 7 | kubenix.project = mkOption { 8 | description = "Name of the project"; 9 | type = types.str; 10 | default = "kubenix"; 11 | }; 12 | 13 | _m.features = mkOption { 14 | description = "List of features exposed by module"; 15 | type = types.listOf types.str; 16 | default = [ ]; 17 | }; 18 | 19 | _m.propagate = mkOption { 20 | description = "Module propagation options"; 21 | type = types.listOf (types.submodule ({ config, ... }: { 22 | options = { 23 | features = mkOption { 24 | description = "List of features that submodule has to have to propagate module"; 25 | type = types.listOf types.str; 26 | default = [ ]; 27 | }; 28 | 29 | module = mkOption { 30 | description = "Module to propagate"; 31 | type = types.unspecified; 32 | default = { }; 33 | }; 34 | }; 35 | })); 36 | default = [ ]; 37 | }; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2003-2019 Jaka Hudoklin and the X-Truder contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /pkgs/applications/networking/cluster/kubernetes/fixup-addonmanager-lib-path.patch: -------------------------------------------------------------------------------- 1 | diff --git a/cluster/addons/addon-manager/kube-addons-main.sh b/cluster/addons/addon-manager/kube-addons-main.sh 2 | index 849973470d1..e4fef30eaea 100755 3 | --- a/cluster/addons/addon-manager/kube-addons-main.sh 4 | +++ b/cluster/addons/addon-manager/kube-addons-main.sh 5 | @@ -17,17 +17,7 @@ 6 | # Import required functions. The addon manager is installed to /opt in 7 | # production use (see the Dockerfile) 8 | # Disabling shellcheck following files as the full path would be required. 9 | -if [ -f "kube-addons.sh" ]; then 10 | - # shellcheck disable=SC1091 11 | - source "kube-addons.sh" 12 | -elif [ -f "/opt/kube-addons.sh" ]; then 13 | - # shellcheck disable=SC1091 14 | - source "/opt/kube-addons.sh" 15 | -else 16 | - # If the required source is missing, we have to fail. 17 | - log ERR "== Could not find kube-addons.sh (not in working directory or /opt) at $(date -Is) ==" 18 | - exit 1 19 | -fi 20 | +source "@out@/bin/kube-addons-lib.sh" 21 | 22 | # The business logic for whether a given object should be created 23 | # was already enforced by salt, and /etc/kubernetes/addons is the 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Kubenix 2.0 is in still work in progress, expect breaking changes** 2 | 3 | # KubeNix 4 | 5 | > Kubernetes resource builder written in nix 6 | 7 | [![Build Status](https://travis-ci.com/xtruder/kubenix.svg?branch=master)](https://travis-ci.com/xtruder/kubenix) 8 | 9 | ## About 10 | 11 | KubeNix is a kubernetes resource builder, that uses nix module system for 12 | definition of kubernetes resources and nix build system for building complex 13 | kubernetes resources very easily. 14 | 15 | ## Development 16 | 17 | ### Building tests 18 | 19 | ```shell 20 | nix-build release.nix -A test-results --show-trace 21 | ``` 22 | 23 | **Building single e2e test** 24 | 25 | ``` 26 | nix-build release.nix -A tests.k8s-1_21.testsByName.k8s-crd.test 27 | nix-build release.nix -A tests.k8s-1_21.testsByName..test 28 | ``` 29 | 30 | **Debugging e2e test** 31 | 32 | ``` 33 | nix-build release.nix -A tests.k8s-1_21.testsByName.k8s-crd.test.driver 34 | nix-build release.nix -A tests.k8s-1_21.testsByName..test.driver 35 | resut/bin/nixos-test-driver 36 | testScript; 37 | ``` 38 | 39 | ## License 40 | 41 | [MIT](LICENSE) © [Jaka Hudoklin](https://x-truder.net) 42 | -------------------------------------------------------------------------------- /modules/submodule.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | with lib; 4 | { 5 | imports = [ ./base.nix ]; 6 | 7 | options.submodule = { 8 | name = mkOption { 9 | description = "Module name"; 10 | type = types.str; 11 | }; 12 | 13 | description = mkOption { 14 | description = "Module description"; 15 | type = types.str; 16 | default = ""; 17 | }; 18 | 19 | version = mkOption { 20 | description = "Module version"; 21 | type = types.str; 22 | default = "1.0.0"; 23 | }; 24 | 25 | tags = mkOption { 26 | description = "List of submodule tags"; 27 | type = types.listOf types.str; 28 | default = [ ]; 29 | }; 30 | 31 | exports = mkOption { 32 | description = "Attribute set of functions to export"; 33 | type = types.attrs; 34 | default = { }; 35 | }; 36 | 37 | passthru = mkOption { 38 | description = "Attribute set to passthru"; 39 | type = types.attrs; 40 | default = { }; 41 | }; 42 | 43 | args._empty = mkOption { }; 44 | }; 45 | 46 | config._module.args.args = config.submodule.args; 47 | config._m.features = [ "submodule" ]; 48 | } 49 | -------------------------------------------------------------------------------- /modules/testing/docker.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | with lib; 4 | with import ../../lib/docker { inherit lib pkgs; }; 5 | let 6 | testing = config.testing; 7 | 8 | allImages = unique (flatten (map (t: t.evaled.config.docker.export or [ ]) testing.tests)); 9 | 10 | cfg = config.testing.docker; 11 | 12 | in 13 | { 14 | options.testing.docker = { 15 | registryUrl = mkOption { 16 | description = "Docker registry url"; 17 | type = types.str; 18 | }; 19 | 20 | images = mkOption { 21 | description = "List of images to export"; 22 | type = types.listOf types.package; 23 | }; 24 | 25 | copyScript = mkOption { 26 | description = "Script to copy images to registry"; 27 | type = types.package; 28 | }; 29 | }; 30 | 31 | config.testing.docker = { 32 | images = allImages; 33 | 34 | copyScript = copyDockerImages { 35 | images = cfg.images; 36 | dest = "docker://" + cfg.registryUrl; 37 | }; 38 | }; 39 | 40 | config.testing.common = [{ 41 | features = [ "docker" ]; 42 | options = { 43 | _file = "testing.docker.registryUrl"; 44 | docker.registry.url = cfg.registryUrl; 45 | }; 46 | }]; 47 | } 48 | -------------------------------------------------------------------------------- /lib/upstreamables.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs }: 2 | 3 | with lib; 4 | 5 | let self = { 6 | 7 | importYAML = path: importJSON (pkgs.runCommand "yaml-to-json" { } '' 8 | ${pkgs.remarshal}/bin/remarshal -i ${path} -if yaml -of json > $out 9 | ''); 10 | 11 | toYAML = config: builtins.readFile (pkgs.runCommand "to-yaml" { } '' 12 | ${pkgs.remarshal}/bin/remarshal -i ${pkgs.writeText "to-json" (builtins.toJSON config)} -if json -of yaml > $out 13 | ''); 14 | 15 | toMultiDocumentYaml = name: documents: pkgs.runCommand name { } 16 | (concatMapStringsSep "\necho --- >> $out\n" 17 | (d: 18 | "${pkgs.remarshal}/bin/remarshal -i ${builtins.toFile "doc" (builtins.toJSON d)} -if json -of yaml >> $out" 19 | ) 20 | documents); 21 | 22 | toBase64 = value: 23 | builtins.readFile 24 | (pkgs.runCommand "value-to-b64" { } "echo -n '${value}' | ${pkgs.coreutils}/bin/base64 -w0 > $out"); 25 | 26 | exp = base: exp: foldr (value: acc: acc * base) 1 (range 1 exp); 27 | 28 | octalToDecimal = value: (foldr 29 | (char: acc: { 30 | i = acc.i + 1; 31 | value = acc.value + (toInt char) * (self.exp 8 acc.i); 32 | }) 33 | { i = 0; value = 0; } 34 | (stringToCharacters value)).value; 35 | }; 36 | in self 37 | -------------------------------------------------------------------------------- /lib/helm/test.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | let 3 | fetchhelm = pkgs.callPackage ./fetchhelm.nix { }; 4 | chart2json = pkgs.callPackage ./chart2json.nix { }; 5 | in 6 | rec { 7 | postgresql-chart = fetchhelm { 8 | chart = "stable/postgresql"; 9 | version = "0.18.1"; 10 | sha256 = "1p3gfmaakxrqb4ncj6nclyfr5afv7xvcdw95c6qyazfg72h3zwjn"; 11 | }; 12 | 13 | istio-chart = fetchhelm { 14 | chart = "istio"; 15 | version = "1.1.0"; 16 | repo = "https://storage.googleapis.com/istio-release/releases/1.1.0-rc.0/charts"; 17 | sha256 = "0ippv2914hwpsb3kkhk8d839dii5whgrhxjwhpb9vdwgji5s7yfl"; 18 | }; 19 | 20 | istio-official-chart = pkgs.fetchgit { 21 | url = "https://github.com/fyery-chen/istio-helm"; 22 | rev = "47e235e775314daeb88a3a53689ed66c396ecd3f"; 23 | sha256 = "190sfyvhdskw6ijy8cprp6hxaazn7s7mg5ids4snshk1pfdg2q8h"; 24 | }; 25 | 26 | postgresql-json = chart2json { 27 | name = "postgresql"; 28 | chart = postgresql-chart; 29 | values = { 30 | networkPolicy.enabled = true; 31 | }; 32 | }; 33 | 34 | istio-json = chart2json { 35 | name = "istio"; 36 | chart = istio-chart; 37 | }; 38 | 39 | istio-official-json = chart2json { 40 | name = "istio-official"; 41 | chart = "${istio-official-chart}/istio-official"; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /jobs/generators/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib }: 2 | let 3 | 4 | generateIstio = import ./istio { 5 | inherit 6 | pkgs 7 | lib 8 | ; 9 | }; 10 | 11 | generateK8S = name: spec: import ./k8s { 12 | inherit 13 | name 14 | pkgs 15 | lib 16 | spec 17 | ; 18 | }; 19 | 20 | in 21 | { 22 | 23 | istio = pkgs.linkFarm "istio-generated" [{ 24 | name = "latest.nix"; 25 | path = generateIstio; 26 | }]; 27 | 28 | k8s = pkgs.linkFarm "k8s-generated" [ 29 | { 30 | name = "v1.19.nix"; 31 | path = generateK8S "v1.19" (builtins.fetchurl { 32 | url = "https://github.com/kubernetes/kubernetes/raw/v1.19.10/api/openapi-spec/swagger.json"; 33 | sha256 = "sha256-ZXxonUAUxRK6rhTgK62ytTdDKCuOoWPwxJmktiKgcJc="; 34 | }); 35 | } 36 | 37 | { 38 | name = "v1.20.nix"; 39 | path = generateK8S "v1.20" (builtins.fetchurl { 40 | url = "https://github.com/kubernetes/kubernetes/raw/v1.20.6/api/openapi-spec/swagger.json"; 41 | sha256 = "sha256-xzVOarQDSomHMimpt8H6MfpiQrLl9am2fDvk/GfLkDw="; 42 | }); 43 | } 44 | 45 | { 46 | name = "v1.21.nix"; 47 | path = generateK8S "v1.21" (builtins.fetchurl { 48 | url = "https://github.com/kubernetes/kubernetes/raw/v1.21.0/api/openapi-spec/swagger.json"; 49 | sha256 = "sha256-EoqYTbtaTlzs7vneoNtXUmdnjTM/U+1gYwCiEy0lOcw="; 50 | }); 51 | } 52 | ]; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /lib/helm/fetchhelm.nix: -------------------------------------------------------------------------------- 1 | { stdenvNoCC, lib, kubernetes-helm, cacert }: 2 | let 3 | cleanName = name: lib.replaceStrings [ "/" ] [ "-" ] name; 4 | 5 | in 6 | { 7 | # name of the chart 8 | chart 9 | 10 | # chart url to fetch from custom location 11 | , chartUrl ? null 12 | 13 | # version of the chart 14 | , version ? null 15 | 16 | # chart hash 17 | , sha256 18 | 19 | # whether to extract chart 20 | , untar ? true 21 | 22 | # use custom charts repo 23 | , repo ? null 24 | 25 | # pass --verify to helm chart 26 | , verify ? false 27 | 28 | # pass --devel to helm chart 29 | , devel ? false 30 | }: stdenvNoCC.mkDerivation { 31 | name = "${cleanName chart}-${if version == null then "dev" else version}"; 32 | 33 | buildCommand = '' 34 | export HOME="$PWD" 35 | echo "adding helm repo" 36 | ${if repo == null then "" else "helm repo add repository ${repo}"} 37 | echo "fetching helm chart" 38 | helm fetch -d ./chart \ 39 | ${if untar then "--untar" else ""} \ 40 | ${if version == null then "" else "--version ${version}"} \ 41 | ${if devel then "--devel" else ""} \ 42 | ${if verify then "--verify" else ""} \ 43 | ${if chartUrl == null then (if repo == null then chart else "repository/${chart}") else chartUrl} 44 | cp -r chart/*/ $out 45 | ''; 46 | outputHashMode = "recursive"; 47 | outputHashAlgo = "sha256"; 48 | outputHash = sha256; 49 | nativeBuildInputs = [ kubernetes-helm cacert ]; 50 | } 51 | -------------------------------------------------------------------------------- /modules/testing/test-options.nix: -------------------------------------------------------------------------------- 1 | { lib, config, pkgs, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.test; 6 | 7 | in 8 | { 9 | options.test = { 10 | name = mkOption { 11 | description = "Test name"; 12 | type = types.str; 13 | }; 14 | 15 | description = mkOption { 16 | description = "Test description"; 17 | type = types.str; 18 | }; 19 | 20 | enable = mkOption { 21 | description = "Whether to enable test"; 22 | type = types.bool; 23 | default = true; 24 | }; 25 | 26 | assertions = mkOption { 27 | type = types.listOf (types.submodule { 28 | options = { 29 | assertion = mkOption { 30 | description = "assertion value"; 31 | type = types.bool; 32 | default = false; 33 | }; 34 | 35 | message = mkOption { 36 | description = "assertion message"; 37 | type = types.str; 38 | }; 39 | }; 40 | }); 41 | default = [ ]; 42 | example = [{ assertion = false; message = "you can't enable this for some reason"; }]; 43 | description = '' 44 | This option allows modules to express conditions that must 45 | hold for the evaluation of the system configuration to 46 | succeed, along with associated error messages for the user. 47 | ''; 48 | }; 49 | 50 | script = mkOption { 51 | description = "Test script to use for e2e test"; 52 | type = types.nullOr (types.either types.lines types.path); 53 | default = null; 54 | }; 55 | 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /examples/nginx-deployment/default.nix: -------------------------------------------------------------------------------- 1 | { evalModules, registry }: 2 | 3 | let 4 | # evaluated configuration 5 | config = (evalModules { 6 | module = 7 | { kubenix, ... }: { 8 | imports = [ 9 | kubenix.modules.testing 10 | ./module.nix 11 | ]; 12 | 13 | # commonalities 14 | kubenix.project = "nginx-deployment-example"; 15 | docker.registry.url = registry; 16 | kubernetes.version = "1.21"; 17 | 18 | testing = { 19 | tests = [ ./test.nix ]; 20 | docker.registryUrl = ""; 21 | # testing commonalities for tests that exhibit the respective feature 22 | common = [ 23 | { 24 | features = [ "k8s" ]; 25 | options = { 26 | kubernetes.version = "1.20"; 27 | }; 28 | } 29 | ]; 30 | }; 31 | }; 32 | }).config; 33 | 34 | in 35 | { 36 | inherit config; 37 | 38 | # config checks 39 | checks = config.testing.success; 40 | 41 | # TODO: e2e test 42 | # test = config.testing.result; 43 | 44 | # nixos test script for running the test 45 | test-script = config.testing.testsByName.nginx-deployment.script; 46 | 47 | # genreated kubernetes List object 48 | generated = config.kubernetes.generated; 49 | 50 | # JSON file you can deploy to kubernetes 51 | result = config.kubernetes.result; 52 | 53 | # Exported docker images 54 | images = config.docker.export; 55 | 56 | # script to push docker images to registry 57 | pushDockerImages = config.docker.copyScript; 58 | } 59 | -------------------------------------------------------------------------------- /tests/k8s/defaults.nix: -------------------------------------------------------------------------------- 1 | { config, lib, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | pod1 = config.kubernetes.api.resources.pods.pod1; 6 | pod2 = config.kubernetes.api.resources.pods.pod2; 7 | in 8 | { 9 | imports = with kubenix.modules; [ test k8s ]; 10 | 11 | test = { 12 | name = "k8s-defaults"; 13 | description = "Simple k8s testing wheter name, apiVersion and kind are preset"; 14 | assertions = [{ 15 | message = "Should have label set with resource"; 16 | assertion = pod1.metadata.labels.resource-label == "value"; 17 | } 18 | { 19 | message = "Should have default label set with group, version, kind"; 20 | assertion = pod1.metadata.labels.gvk-label == "value"; 21 | } 22 | { 23 | message = "Should have conditional annotation set"; 24 | assertion = pod2.metadata.annotations.conditional-annotation == "value"; 25 | }]; 26 | }; 27 | 28 | kubernetes.resources.pods.pod1 = { }; 29 | 30 | kubernetes.resources.pods.pod2 = { 31 | metadata.labels.custom-label = "value"; 32 | }; 33 | 34 | kubernetes.api.defaults = [{ 35 | resource = "pods"; 36 | default.metadata.labels.resource-label = "value"; 37 | } 38 | { 39 | group = "core"; 40 | kind = "Pod"; 41 | version = "v1"; 42 | default.metadata.labels.gvk-label = "value"; 43 | } 44 | { 45 | resource = "pods"; 46 | default = { config, ... }: { 47 | config.metadata.annotations = mkIf (config.metadata.labels ? "custom-label") { 48 | conditional-annotation = "value"; 49 | }; 50 | }; 51 | }]; 52 | } 53 | -------------------------------------------------------------------------------- /tests/submodules/passthru.nix: -------------------------------------------------------------------------------- 1 | { name, config, lib, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | submodule = { name, ... }: { 6 | imports = [ kubenix.modules.submodule ]; 7 | 8 | config.submodule = { 9 | name = "subm"; 10 | passthru.global.${name} = "true"; 11 | }; 12 | }; 13 | in 14 | { 15 | imports = with kubenix.modules; [ test submodules ]; 16 | 17 | options = { 18 | global = mkOption { 19 | description = "Global value"; 20 | type = types.attrs; 21 | default = { }; 22 | }; 23 | }; 24 | 25 | config = { 26 | test = { 27 | name = "submodules-passthru"; 28 | description = "Submodules passthru test"; 29 | assertions = [{ 30 | message = "should passthru values if passthru enabled"; 31 | assertion = hasAttr "inst1" config.global && config.global.inst1 == "true"; 32 | } 33 | { 34 | message = "should not passthru values if passthru not enabled"; 35 | assertion = !(hasAttr "inst2" config.global); 36 | } 37 | { 38 | message = "should passthru by default"; 39 | assertion = hasAttr "inst3" config.global && config.global.inst3 == "true"; 40 | }]; 41 | }; 42 | 43 | submodules.imports = [{ 44 | modules = [ submodule ]; 45 | }]; 46 | 47 | submodules.instances.inst1 = { 48 | submodule = "subm"; 49 | passthru.enable = true; 50 | }; 51 | 52 | submodules.instances.inst2 = { 53 | submodule = "subm"; 54 | passthru.enable = false; 55 | }; 56 | 57 | submodules.instances.inst3 = { 58 | submodule = "subm"; 59 | }; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /tests/default.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem 2 | , evalModules ? (import ../. { }).evalModules.${system} 3 | }: 4 | 5 | { k8sVersion ? "1.21" 6 | , registry ? throw "Registry url not defined" 7 | , doThrowError ? true # whether any testing error should throw an error 8 | , enabledTests ? null 9 | }: 10 | 11 | let 12 | config = (evalModules { 13 | 14 | module = 15 | { kubenix, pkgs, ... }: { 16 | 17 | imports = [ kubenix.modules.testing ]; 18 | 19 | testing = { 20 | inherit doThrowError enabledTests; 21 | name = "kubenix-${k8sVersion}"; 22 | tests = [ 23 | ./k8s/simple.nix 24 | ./k8s/deployment.nix 25 | # ./k8s/crd.nix # flaky 26 | ./k8s/defaults.nix 27 | ./k8s/order.nix 28 | ./k8s/submodule.nix 29 | ./k8s/imports.nix 30 | # ./helm/simple.nix 31 | # ./istio/bookinfo.nix # infinite recursion 32 | ./submodules/simple.nix 33 | ./submodules/defaults.nix 34 | ./submodules/versioning.nix 35 | ./submodules/exports.nix 36 | ./submodules/passthru.nix 37 | ]; 38 | 39 | args = { images = pkgs.callPackage ./images.nix { }; }; 40 | docker.registryUrl = registry; 41 | 42 | common = [ 43 | { 44 | features = [ "k8s" ]; 45 | options = { 46 | kubernetes.version = k8sVersion; 47 | }; 48 | } 49 | ]; 50 | }; 51 | 52 | }; 53 | 54 | }).config; 55 | in 56 | config.testing // { recurseForDerivations = true; } 57 | -------------------------------------------------------------------------------- /tests/images.nix: -------------------------------------------------------------------------------- 1 | { pkgs, dockerTools, lib, ... }: 2 | 3 | with lib; 4 | 5 | { 6 | curl = dockerTools.buildLayeredImage { 7 | name = "curl"; 8 | tag = "latest"; 9 | config.Cmd = [ "${pkgs.bash}" "-c" "sleep infinity" ]; 10 | contents = [ pkgs.bash pkgs.curl pkgs.cacert ]; 11 | }; 12 | 13 | nginx = 14 | let 15 | nginxPort = "80"; 16 | nginxConf = pkgs.writeText "nginx.conf" '' 17 | user nginx nginx; 18 | daemon off; 19 | error_log /dev/stdout info; 20 | pid /dev/null; 21 | events {} 22 | http { 23 | access_log /dev/stdout; 24 | server { 25 | listen ${nginxPort}; 26 | index index.html; 27 | location / { 28 | root ${nginxWebRoot}; 29 | } 30 | } 31 | } 32 | ''; 33 | nginxWebRoot = pkgs.writeTextDir "index.html" '' 34 |

Hello from NGINX

35 | ''; 36 | in 37 | dockerTools.buildLayeredImage { 38 | name = "xtruder/nginx"; 39 | tag = "latest"; 40 | contents = [ pkgs.nginx ]; 41 | extraCommands = '' 42 | mkdir -p etc 43 | chmod u+w etc 44 | mkdir -p var/cache/nginx 45 | chmod u+w var/cache/nginx 46 | mkdir -p var/log/nginx 47 | chmod u+w var/log/nginx 48 | echo "nginx:x:1000:1000::/:" > etc/passwd 49 | echo "nginx:x:1000:nginx" > etc/group 50 | ''; 51 | config = { 52 | Cmd = [ "nginx" "-c" nginxConf ]; 53 | ExposedPorts = { 54 | "${nginxPort}/tcp" = { }; 55 | }; 56 | }; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devshell-flake": { 4 | "locked": { 5 | "lastModified": 1622013274, 6 | "narHash": "sha256-mK/Lv0lCbl07dI5s7tR/7nb79HunKnJik3KyR6yeI2k=", 7 | "owner": "numtide", 8 | "repo": "devshell", 9 | "rev": "e7faf69e6bf8546517cc936c7f6d31c7eb3abcb2", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "devshell", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-utils": { 19 | "locked": { 20 | "lastModified": 1620759905, 21 | "narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=", 22 | "owner": "numtide", 23 | "repo": "flake-utils", 24 | "rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "type": "github" 31 | } 32 | }, 33 | "nixpkgs": { 34 | "locked": { 35 | "lastModified": 1622230509, 36 | "narHash": "sha256-ybr8ufMIE1OVIG+S7cMihx1HlrJgTV3lLr/oW/LplTM=", 37 | "owner": "NixOS", 38 | "repo": "nixpkgs", 39 | "rev": "190d0579fbb13e83756dc2e6df49a3b9221fbfa9", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "NixOS", 44 | "repo": "nixpkgs", 45 | "type": "github" 46 | } 47 | }, 48 | "root": { 49 | "inputs": { 50 | "devshell-flake": "devshell-flake", 51 | "flake-utils": "flake-utils", 52 | "nixpkgs": "nixpkgs" 53 | } 54 | } 55 | }, 56 | "root": "root", 57 | "version": 7 58 | } 59 | -------------------------------------------------------------------------------- /lib/helm/chart2json.nix: -------------------------------------------------------------------------------- 1 | { stdenvNoCC, lib, kubernetes-helm, gawk, remarshal, jq }: 2 | 3 | with lib; 4 | 5 | { 6 | # chart to template 7 | chart 8 | 9 | # release name 10 | , name 11 | 12 | # namespace to install release into 13 | , namespace ? null 14 | 15 | # values to pass to chart 16 | , values ? { } 17 | 18 | # kubernetes version to template chart for 19 | , kubeVersion ? null 20 | }: 21 | let 22 | valuesJsonFile = builtins.toFile "${name}-values.json" (builtins.toJSON values); 23 | in 24 | stdenvNoCC.mkDerivation { 25 | name = "${name}.json"; 26 | buildCommand = '' 27 | # template helm file and write resources to yaml 28 | helm template "${name}" \ 29 | ${optionalString (kubeVersion != null) "--api-versions ${kubeVersion}"} \ 30 | ${optionalString (namespace != null) "--namespace ${namespace}"} \ 31 | ${optionalString (values != { }) "-f ${valuesJsonFile}"} \ 32 | ${chart} >resources.yaml 33 | 34 | # split multy yaml file into multiple files 35 | awk 'BEGIN{i=1}{line[i++]=$0}END{j=1;n=0; while (j>"resource-"n".yaml"; j++}}' resources.yaml 36 | 37 | # join multiple yaml files in jsonl file 38 | for file in ./resource-*.yaml 39 | do 40 | remarshal -i $file -if yaml -of json >>resources.jsonl 41 | done 42 | 43 | # convert jsonl file to json array, remove null values and write to $out 44 | cat resources.jsonl | jq -Scs 'walk( 45 | if type == "object" then 46 | with_entries(select(.value != null)) 47 | elif type == "array" then 48 | map(select(. != null)) 49 | else 50 | . 51 | end)' > $out 52 | ''; 53 | nativeBuildInputs = [ kubernetes-helm gawk remarshal jq ]; 54 | } 55 | -------------------------------------------------------------------------------- /tests/k8s/submodule.nix: -------------------------------------------------------------------------------- 1 | { name, config, lib, kubenix, images, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.submodules.instances.passthru; 6 | in 7 | { 8 | imports = with kubenix.modules; [ test submodules k8s docker ]; 9 | 10 | test = { 11 | name = "k8s-submodule"; 12 | description = "Simple k8s submodule test"; 13 | assertions = [{ 14 | message = "Submodule has correct name set"; 15 | assertion = (head config.kubernetes.objects).metadata.name == "passthru"; 16 | } 17 | { 18 | message = "Should expose docker image"; 19 | assertion = (head config.docker.export).imageName == "xtruder/nginx"; 20 | }]; 21 | }; 22 | 23 | kubernetes.namespace = "test-namespace"; 24 | 25 | submodules.imports = [{ 26 | module = { name, config, ... }: { 27 | imports = with kubenix.modules; [ submodule k8s docker ]; 28 | 29 | config = { 30 | submodule = { 31 | name = "test-submodule"; 32 | passthru = { 33 | kubernetes.objects = config.kubernetes.objects; 34 | docker.images = config.docker.images; 35 | }; 36 | }; 37 | 38 | kubernetes.resources.pods.nginx = { 39 | metadata.name = name; 40 | spec.containers.nginx.image = config.docker.images.nginx.path; 41 | }; 42 | 43 | docker.images.nginx.image = images.nginx; 44 | }; 45 | }; 46 | }]; 47 | 48 | kubernetes.api.defaults = [{ 49 | propagate = true; 50 | default.metadata.labels.my-label = "my-value"; 51 | }]; 52 | 53 | submodules.instances.passthru = { 54 | submodule = "test-submodule"; 55 | }; 56 | 57 | submodules.instances.no-passthru = { 58 | submodule = "test-submodule"; 59 | passthru.enable = false; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /examples/nginx-deployment/module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | nginx = pkgs.callPackage ./image.nix { }; 6 | in 7 | { 8 | imports = with kubenix.modules; [ k8s docker ]; 9 | 10 | docker.images.nginx.image = nginx; 11 | 12 | kubernetes.resources.deployments.nginx = { 13 | spec = { 14 | replicas = 10; 15 | selector.matchLabels.app = "nginx"; 16 | template = { 17 | metadata.labels.app = "nginx"; 18 | spec = { 19 | securityContext.fsGroup = 1000; 20 | containers.nginx = { 21 | image = config.docker.images.nginx.path; 22 | imagePullPolicy = "IfNotPresent"; 23 | volumeMounts."/etc/nginx".name = "config"; 24 | volumeMounts."/var/lib/html".name = "static"; 25 | }; 26 | volumes.config.configMap.name = "nginx-config"; 27 | volumes.static.configMap.name = "nginx-static"; 28 | }; 29 | }; 30 | }; 31 | }; 32 | 33 | kubernetes.resources.configMaps.nginx-config.data."nginx.conf" = '' 34 | user nginx nginx; 35 | daemon off; 36 | error_log /dev/stdout info; 37 | pid /dev/null; 38 | events {} 39 | http { 40 | access_log /dev/stdout; 41 | server { 42 | listen 80; 43 | index index.html; 44 | location / { 45 | root /var/lib/html; 46 | } 47 | } 48 | } 49 | ''; 50 | 51 | kubernetes.resources.configMaps.nginx-static.data."index.html" = '' 52 |

Hello from NGINX

53 | ''; 54 | 55 | kubernetes.resources.services.nginx = { 56 | spec = { 57 | ports = [{ 58 | name = "http"; 59 | port = 80; 60 | }]; 61 | selector.app = "nginx"; 62 | }; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /tests/k8s/order.nix: -------------------------------------------------------------------------------- 1 | { config, lib, kubenix, pkgs, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.kubernetes.api.resources.customResourceDefinitions.crontabs; 6 | in 7 | { 8 | imports = with kubenix.modules; [ test k8s ]; 9 | 10 | test = { 11 | name = "k8s-order"; 12 | description = "test tesing k8s resource order"; 13 | assertions = [{ 14 | message = "should have correct order of resources"; 15 | assertion = 16 | (elemAt config.kubernetes.objects 0).kind == "CustomResourceDefinition" && 17 | (elemAt config.kubernetes.objects 1).kind == "Namespace" && 18 | (elemAt config.kubernetes.objects 2).kind == "CronTab"; 19 | }]; 20 | }; 21 | 22 | kubernetes.resources.customResourceDefinitions.crontabs = { 23 | apiVersion = "apiextensions.k8s.io/v1"; 24 | metadata.name = "crontabs.stable.example.com"; 25 | spec = { 26 | group = "stable.example.com"; 27 | versions = [{ 28 | name = "v1"; 29 | served = true; 30 | schema = true; 31 | }]; 32 | scope = "Namespaced"; 33 | names = { 34 | plural = "crontabs"; 35 | singular = "crontab"; 36 | kind = "CronTab"; 37 | shortNames = [ "ct" ]; 38 | }; 39 | }; 40 | }; 41 | 42 | kubernetes.customTypes = [{ 43 | name = "crontabs"; 44 | description = "CronTabs resources"; 45 | 46 | attrName = "cronTabs"; 47 | group = "stable.example.com"; 48 | version = "v1"; 49 | kind = "CronTab"; 50 | module = { 51 | options.schedule = mkOption { 52 | description = "Crontab schedule script"; 53 | type = types.str; 54 | }; 55 | }; 56 | }]; 57 | 58 | kubernetes.resources.namespaces.test = { }; 59 | 60 | kubernetes.resources."stable.example.com"."v1".CronTab.crontab.spec.schedule = "* * * * *"; 61 | } 62 | -------------------------------------------------------------------------------- /lib/k8s/default.nix: -------------------------------------------------------------------------------- 1 | { lib }: 2 | 3 | with lib; 4 | 5 | rec { 6 | # TODO: refactor with mkOptionType 7 | mkSecretOption = { description ? "", default ? { }, allowNull ? true }: mkOption { 8 | inherit description; 9 | type = (if allowNull then types.nullOr else id) (types.submodule { 10 | options = { 11 | name = mkOption ({ 12 | description = "Name of the secret where secret is stored"; 13 | type = types.str; 14 | default = default.name; 15 | } // (optionalAttrs (default ? "name") { 16 | default = default.name; 17 | })); 18 | 19 | key = mkOption ({ 20 | description = "Name of the key where secret is stored"; 21 | type = types.str; 22 | } // (optionalAttrs (default ? "key") { 23 | default = default.key; 24 | })); 25 | }; 26 | }); 27 | default = if default == null then null else { }; 28 | }; 29 | 30 | secretToEnv = value: { 31 | valueFrom.secretKeyRef = { 32 | inherit (value) name key; 33 | }; 34 | }; 35 | 36 | # Creates kubernetes list from a list of kubernetes objects 37 | mkList = { items, labels ? { } }: { 38 | kind = "List"; 39 | apiVersion = "v1"; 40 | 41 | inherit items labels; 42 | }; 43 | 44 | # Creates hashed kubernetes list from a list of kubernetes objects 45 | mkHashedList = { items, labels ? { } }: 46 | let 47 | hash = builtins.hashString "sha1" (builtins.toJSON items); 48 | 49 | labeledItems = map 50 | (item: recursiveUpdate item { 51 | metadata.labels."kubenix/hash" = hash; 52 | }) 53 | items; 54 | 55 | in 56 | mkList { 57 | items = labeledItems; 58 | labels = { 59 | "kubenix/hash" = hash; 60 | } // labels; 61 | }; 62 | 63 | toBase64 = lib.toBase64; 64 | octalToDecimal = lib.octalToDecimal; 65 | } 66 | -------------------------------------------------------------------------------- /tests/submodules/simple.nix: -------------------------------------------------------------------------------- 1 | { name, config, lib, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.submodules.instances.instance; 6 | args = cfg.config.submodule.args; 7 | in 8 | { 9 | imports = with kubenix.modules; [ test submodules ]; 10 | 11 | test = { 12 | name = "submodules-simple"; 13 | description = "Simple k8s submodule test"; 14 | assertions = [{ 15 | message = "Submodule name is set"; 16 | assertion = cfg.name == "instance"; 17 | } 18 | { 19 | message = "Submodule version is set"; 20 | assertion = cfg.version == null; 21 | } 22 | { 23 | message = "Submodule config has submodule definition"; 24 | assertion = cfg.config.submodule.name == "submodule"; 25 | } 26 | { 27 | message = "Should have argument set"; 28 | assertion = args.value == "test"; 29 | } 30 | { 31 | message = "Should have submodule name set"; 32 | assertion = args.name == "instance"; 33 | } 34 | { 35 | message = "should have tag set"; 36 | assertion = elem "tag" (cfg.config.submodule.tags); 37 | }]; 38 | }; 39 | 40 | submodules.propagate.enable = true; 41 | submodules.imports = [{ 42 | module = { submodule, ... }: { 43 | imports = [ kubenix.modules.submodule ]; 44 | 45 | options.submodule.args = { 46 | name = mkOption { 47 | description = "Submodule name"; 48 | type = types.str; 49 | default = submodule.name; 50 | }; 51 | value = mkOption { 52 | description = "Submodule argument"; 53 | type = types.str; 54 | }; 55 | }; 56 | 57 | config = { 58 | submodule.name = "submodule"; 59 | submodule.tags = [ "tag" ]; 60 | }; 61 | }; 62 | }]; 63 | 64 | submodules.instances.instance = { 65 | submodule = "submodule"; 66 | args = { 67 | value = "test"; 68 | }; 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /modules/testing/driver/kubetest.nix: -------------------------------------------------------------------------------- 1 | { lib, config, pkgs, ... }: 2 | 3 | with lib; 4 | let 5 | testing = config.testing; 6 | cfg = testing.driver.kubetest; 7 | 8 | kubetest = import ./kubetestdrv.nix { inherit pkgs; }; 9 | 10 | pythonEnv = pkgs.python38.withPackages (ps: with ps; [ 11 | pytest 12 | kubetest 13 | kubernetes 14 | ] ++ cfg.extraPackages); 15 | 16 | toTestScript = t: 17 | if isString t.script 18 | then 19 | pkgs.writeText "${t.name}.py" '' 20 | ${cfg.defaultHeader} 21 | ${t.script} 22 | '' 23 | else t.script; 24 | 25 | tests = let 26 | # make sure tests are prefixed so that alphanumerical 27 | # sorting reproduces them in the same order as they 28 | # have been declared in the list. 29 | seive = t: t.script != null && t.enabled; 30 | allEligibleTests = filter seive testing.tests; 31 | listLengthPadding = builtins.length ( 32 | lib.stringToCharacters ( 33 | builtins.toString ( 34 | builtins.length allEligibleTests))); 35 | op = 36 | (i: t: { 37 | path = toTestScript t; 38 | name = let 39 | prefix = lib.fixedWidthNumber listLengthPadding i; 40 | in "${prefix}_${t.name}_test.py"; 41 | }); 42 | in pkgs.linkFarm "${testing.name}-tests" ( 43 | lib.imap0 op allEligibleTests; 44 | ); 45 | 46 | testScript = pkgs.writeScript "test-${testing.name}.sh" '' 47 | #!/usr/bin/env bash 48 | ${pythonEnv}/bin/pytest -p no:cacheprovider ${tests} $@ 49 | ''; 50 | 51 | in 52 | { 53 | options.testing.driver.kubetest = { 54 | defaultHeader = mkOption { 55 | type = types.lines; 56 | description = "Default test header"; 57 | default = '' 58 | import pytest 59 | ''; 60 | }; 61 | 62 | extraPackages = mkOption { 63 | type = types.listOf types.package; 64 | description = "Extra packages to pass to tests"; 65 | default = [ ]; 66 | }; 67 | }; 68 | 69 | config.testing.testScript = testScript; 70 | } 71 | -------------------------------------------------------------------------------- /tests/submodules/versioning.nix: -------------------------------------------------------------------------------- 1 | { name, config, lib, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | inst-exact = config.submodules.instances.inst-exact.config; 6 | inst-regex = config.submodules.instances.inst-regex.config; 7 | inst-latest = config.submodules.instances.inst-latest.config; 8 | 9 | submodule = { 10 | imports = [ kubenix.modules.submodule ]; 11 | 12 | options.version = mkOption { 13 | type = types.str; 14 | default = "undefined"; 15 | }; 16 | 17 | config.submodule.name = "subm"; 18 | }; 19 | in 20 | { 21 | imports = with kubenix.modules; [ test submodules ]; 22 | 23 | test = { 24 | name = "submodules-versioning"; 25 | description = "Submodules versioning test"; 26 | assertions = [{ 27 | message = "should select exact version"; 28 | assertion = inst-exact.version == "1.1.0"; 29 | } 30 | { 31 | message = "should select regex version"; 32 | assertion = inst-regex.version == "1.2.1"; 33 | } 34 | { 35 | message = "should select latest version"; 36 | assertion = inst-latest.version == "1.2.1"; 37 | }]; 38 | }; 39 | 40 | submodules.imports = [{ 41 | modules = [{ 42 | config.submodule.version = "1.0.0"; 43 | config.version = "1.0.0"; 44 | } 45 | submodule]; 46 | } 47 | { 48 | modules = [{ 49 | config.submodule.version = "1.1.0"; 50 | config.version = "1.1.0"; 51 | } 52 | submodule]; 53 | } 54 | { 55 | modules = [{ 56 | config.submodule.version = "1.2.0"; 57 | config.version = "1.2.0"; 58 | } 59 | submodule]; 60 | } 61 | { 62 | modules = [{ 63 | config.submodule.version = "1.2.1"; 64 | config.version = "1.2.1"; 65 | } 66 | submodule]; 67 | }]; 68 | 69 | submodules.instances.inst-exact = { 70 | submodule = "subm"; 71 | version = "1.1.0"; 72 | }; 73 | 74 | submodules.instances.inst-regex = { 75 | submodule = "subm"; 76 | version = "~1.2.*"; 77 | }; 78 | 79 | submodules.instances.inst-latest.submodule = "subm"; 80 | } 81 | -------------------------------------------------------------------------------- /pkgs/applications/networking/cluster/kubernetes/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , fetchFromGitHub 4 | , removeReferencesTo 5 | , which 6 | , go 7 | , makeWrapper 8 | , rsync 9 | , installShellFiles 10 | 11 | , components ? [ 12 | "cmd/kubelet" 13 | "cmd/kube-apiserver" 14 | "cmd/kube-controller-manager" 15 | "cmd/kube-proxy" 16 | "cmd/kube-scheduler" 17 | "test/e2e/e2e.test" 18 | ] 19 | }: 20 | 21 | stdenv.mkDerivation rec { 22 | pname = "kubernetes"; 23 | version = "1.20.4"; 24 | 25 | src = fetchFromGitHub { 26 | owner = "kubernetes"; 27 | repo = "kubernetes"; 28 | rev = "v${version}"; 29 | hash = "sha256-r9Clwr+87Ns4VXUW9F6cgks+LknY39ngbQgZ5UMZ0Vo="; 30 | }; 31 | 32 | nativeBuildInputs = [ removeReferencesTo makeWrapper which go rsync installShellFiles ]; 33 | 34 | outputs = [ "out" "man" "pause" ]; 35 | 36 | patches = [ ./fixup-addonmanager-lib-path.patch ]; 37 | 38 | postPatch = '' 39 | # go env breaks the sandbox 40 | substituteInPlace "hack/lib/golang.sh" \ 41 | --replace 'echo "$(go env GOHOSTOS)/$(go env GOHOSTARCH)"' 'echo "${go.GOOS}/${go.GOARCH}"' 42 | 43 | substituteInPlace "hack/update-generated-docs.sh" --replace "make" "make SHELL=${stdenv.shell}" 44 | # hack/update-munge-docs.sh only performs some tests on the documentation. 45 | # They broke building k8s; disabled for now. 46 | echo "true" > "hack/update-munge-docs.sh" 47 | 48 | patchShebangs ./hack 49 | ''; 50 | 51 | WHAT = lib.concatStringsSep " " ([ 52 | "cmd/kubeadm" 53 | "cmd/kubectl" 54 | ] ++ components); 55 | 56 | postBuild = '' 57 | ./hack/update-generated-docs.sh 58 | (cd build/pause/linux && cc pause.c -o pause) 59 | ''; 60 | 61 | installPhase = '' 62 | for p in $WHAT; do 63 | install -D _output/local/go/bin/''${p##*/} -t $out/bin 64 | done 65 | 66 | install -D build/pause/linux/pause -t $pause/bin 67 | installManPage docs/man/man1/*.[1-9] 68 | 69 | cp ${./mk-docker-opts.sh} $out/bin/mk-docker-opts.sh 70 | 71 | for tool in kubeadm kubectl; do 72 | for shell in bash zsh; do 73 | $out/bin/$tool completion $shell > $tool.$shell 74 | installShellCompletion $tool.$shell 75 | done 76 | done 77 | ''; 78 | 79 | preFixup = '' 80 | find $out/bin $pause/bin -type f -exec remove-references-to -t ${go} '{}' + 81 | ''; 82 | 83 | meta = with lib; { 84 | description = "Production-Grade Container Scheduling and Management"; 85 | license = licenses.asl20; 86 | homepage = "https://kubernetes.io"; 87 | maintainers = with maintainers; [ johanot offline saschagrunert ]; 88 | platforms = platforms.unix; 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /modules/testing/runtime/nixos-k8s.nix: -------------------------------------------------------------------------------- 1 | # nixos-k8s implements nixos kubernetes testing runtime 2 | 3 | { config 4 | , pkgs 5 | , lib 6 | , ... 7 | }: 8 | 9 | with lib; 10 | let 11 | testing = config.testing; 12 | # kubeconfig = "/etc/${config.services.kubernetes.pki.etcClusterAdminKubeconfig}"; 13 | kubeconfig = "/etc/kubernetes/cluster-admin.kubeconfig"; 14 | kubecerts = "/var/lib/kubernetes/secrets"; 15 | 16 | # how we differ from the standard configuration of mkKubernetesBaseTest 17 | extraConfiguration = { config, pkgs, lib, nodes, ... }: { 18 | 19 | virtualisation = { 20 | memorySize = 2048; 21 | }; 22 | 23 | networking = { 24 | nameservers = [ "10.0.0.254" ]; 25 | firewall = { 26 | trustedInterfaces = [ "docker0" "cni0" ]; 27 | }; 28 | }; 29 | 30 | services.kubernetes = { 31 | flannel.enable = false; 32 | kubelet = { 33 | seedDockerImages = testing.docker.images; 34 | networkPlugin = "cni"; 35 | cni.config = [{ 36 | name = "mynet"; 37 | type = "bridge"; 38 | bridge = "cni0"; 39 | addIf = true; 40 | ipMasq = true; 41 | isGateway = true; 42 | ipam = { 43 | type = "host-local"; 44 | subnet = "10.1.0.0/16"; 45 | gateway = "10.1.0.1"; 46 | routes = [{ 47 | dst = "0.0.0.0/0"; 48 | }]; 49 | }; 50 | }]; 51 | }; 52 | }; 53 | 54 | systemd = { 55 | extraConfig = "DefaultLimitNOFILE=1048576"; 56 | # Host tools should have a chance to access guest's kube api 57 | services.copy-certs = { 58 | description = "Share k8s certificates with host"; 59 | script = "cp -rf ${kubecerts} /tmp/xchg/; cp -f ${kubeconfig} /tmp/xchg/;"; 60 | after = [ "kubernetes.target" ]; 61 | wantedBy = [ "multi-user.target" ]; 62 | serviceConfig = { 63 | Type = "oneshot"; 64 | RemainAfterExit = true; 65 | }; 66 | }; 67 | }; 68 | 69 | }; 70 | 71 | script = '' 72 | machine1.succeed("${testing.testScript} --kube-config=${kubeconfig}") 73 | ''; 74 | 75 | test = 76 | with import "${pkgs.path}/nixos/tests/kubernetes/base.nix" { inherit pkgs; inherit (pkgs) system; }; 77 | mkKubernetesSingleNodeTest { 78 | inherit extraConfiguration; 79 | inherit (config.testing) name; 80 | test = script; 81 | }; 82 | 83 | 84 | in 85 | { 86 | options.testing.runtime.nixos-k8s = { 87 | driver = mkOption { 88 | description = "Test driver"; 89 | type = types.package; 90 | internal = true; 91 | }; 92 | }; 93 | 94 | config.testing.runtime.nixos-k8s.driver = test.driver; 95 | } 96 | -------------------------------------------------------------------------------- /modules/docker.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, docker, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.docker; 6 | in 7 | { 8 | imports = [ ./base.nix ]; 9 | 10 | options.docker = { 11 | registry.url = mkOption { 12 | description = "Default registry url where images are published"; 13 | type = types.str; 14 | default = ""; 15 | }; 16 | 17 | images = mkOption { 18 | description = "Attribute set of docker images that should be published"; 19 | type = types.attrsOf (types.submodule ({ name, config, ... }: { 20 | options = { 21 | image = mkOption { 22 | description = "Docker image to publish"; 23 | type = types.nullOr types.package; 24 | default = null; 25 | }; 26 | 27 | name = mkOption { 28 | description = "Desired docker image name"; 29 | type = types.str; 30 | default = builtins.unsafeDiscardStringContext config.image.imageName; 31 | }; 32 | 33 | tag = mkOption { 34 | description = "Desired docker image tag"; 35 | type = types.str; 36 | default = builtins.unsafeDiscardStringContext config.image.imageTag; 37 | }; 38 | 39 | registry = mkOption { 40 | description = "Docker registry url where image is published"; 41 | type = types.str; 42 | default = cfg.registry.url; 43 | }; 44 | 45 | path = mkOption { 46 | description = "Full docker image path"; 47 | type = types.str; 48 | default = 49 | if config.registry != "" 50 | then "${config.registry}/${config.name}:${config.tag}" 51 | else "${config.name}:${config.tag}"; 52 | }; 53 | }; 54 | })); 55 | default = { }; 56 | }; 57 | 58 | export = mkOption { 59 | description = "List of images to export"; 60 | type = types.listOf types.package; 61 | default = [ ]; 62 | }; 63 | 64 | copyScript = mkOption { 65 | description = "Image copy script"; 66 | type = types.package; 67 | default = docker.copyDockerImages { 68 | dest = "docker://${cfg.registry.url}"; 69 | images = cfg.export; 70 | }; 71 | }; 72 | }; 73 | 74 | config = { 75 | # define docker feature 76 | _m.features = [ "docker" ]; 77 | 78 | # propagate docker options if docker feature is enabled 79 | _m.propagate = [{ 80 | features = [ "docker" ]; 81 | module = { config, name, ... }: { 82 | # propagate registry options 83 | docker.registry = cfg.registry; 84 | }; 85 | }]; 86 | 87 | # pass docker library as param 88 | _module.args.docker = import ../lib/docker { inherit lib pkgs; }; 89 | 90 | # list of exported docker images 91 | docker.export = mapAttrsToList (_: i: i.image) 92 | (filterAttrs (_: i: i.registry != null) config.docker.images); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /tests/istio/bookinfo.nix: -------------------------------------------------------------------------------- 1 | { config, kubenix, ... }: 2 | 3 | { 4 | imports = with kubenix.modules; [ test k8s istio ]; 5 | 6 | test = { 7 | name = "istio-bookinfo"; 8 | description = "Simple istio bookinfo application (WIP)"; 9 | }; 10 | 11 | kubernetes.api."networking.istio.io"."v1alpha3" = { 12 | Gateway."bookinfo-gateway" = { 13 | spec = { 14 | selector.istio = "ingressgateway"; 15 | servers = [{ 16 | port = { 17 | number = 80; 18 | name = "http"; 19 | protocol = "HTTP"; 20 | }; 21 | hosts = [ "*" ]; 22 | }]; 23 | }; 24 | }; 25 | 26 | VirtualService.bookinfo = { 27 | spec = { 28 | hosts = [ "*" ]; 29 | gateways = [ "bookinfo-gateway" ]; 30 | http = [{ 31 | match = [{ 32 | uri.exact = "/productpage"; 33 | } 34 | { 35 | uri.exact = "/login"; 36 | } 37 | { 38 | uri.exact = "/logout"; 39 | } 40 | { 41 | uri.prefix = "/api/v1/products"; 42 | }]; 43 | route = [{ 44 | destination = { 45 | host = "productpage"; 46 | port.number = 9080; 47 | }; 48 | }]; 49 | }]; 50 | }; 51 | }; 52 | 53 | DestinationRule.productpage = { 54 | spec = { 55 | host = "productpage"; 56 | subsets = [{ 57 | name = "v1"; 58 | labels.version = "v1"; 59 | }]; 60 | }; 61 | }; 62 | 63 | DestinationRule.reviews = { 64 | spec = { 65 | host = "reviews"; 66 | subsets = [{ 67 | name = "v1"; 68 | labels.version = "v1"; 69 | } 70 | { 71 | name = "v2"; 72 | labels.version = "v2"; 73 | } 74 | { 75 | name = "v3"; 76 | labels.version = "v3"; 77 | }]; 78 | }; 79 | }; 80 | 81 | DestinationRule.ratings = { 82 | spec = { 83 | host = "ratings"; 84 | subsets = [{ 85 | name = "v1"; 86 | labels.version = "v1"; 87 | } 88 | { 89 | name = "v2"; 90 | labels.version = "v2"; 91 | } 92 | { 93 | name = "v2-mysql"; 94 | labels.version = "v2-mysql"; 95 | } 96 | { 97 | name = "v2-mysql-vm"; 98 | labels.version = "v2-mysql-vm"; 99 | }]; 100 | }; 101 | }; 102 | 103 | DestinationRule.details = { 104 | spec = { 105 | host = "details"; 106 | subsets = [{ 107 | name = "v1"; 108 | labels.version = "v1"; 109 | } 110 | { 111 | name = "v2"; 112 | labels.version = "v2"; 113 | }]; 114 | }; 115 | }; 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /modules/testing/default.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.testing; 6 | 7 | testModule = { 8 | imports = [ ./evalTest.nix ]; 9 | 10 | # passthru testing configuration 11 | config._module.args = { 12 | inherit pkgs kubenix; 13 | testing = cfg; 14 | }; 15 | }; 16 | 17 | isTestEnabled = test: 18 | (cfg.enabledTests == null || elem test.name cfg.enabledTests) && test.enable; 19 | 20 | in 21 | { 22 | imports = [ 23 | ./docker.nix 24 | ./driver/kubetest.nix 25 | ./runtime/local.nix 26 | ./runtime/nixos-k8s.nix 27 | ]; 28 | 29 | options.testing = { 30 | name = mkOption { 31 | description = "Testing suite name"; 32 | type = types.str; 33 | default = "default"; 34 | }; 35 | 36 | doThrowError = mkOption { 37 | description = "Whether to throw error"; 38 | type = types.bool; 39 | default = true; 40 | }; 41 | 42 | common = mkOption { 43 | description = "List of common options to apply to tests"; 44 | type = types.listOf (types.submodule ({ config, ... }: { 45 | options = { 46 | features = mkOption { 47 | description = "List of features that test has to have to apply options"; 48 | type = types.listOf types.str; 49 | default = [ ]; 50 | }; 51 | 52 | options = mkOption { 53 | description = "Options to apply to test"; 54 | type = types.unspecified; 55 | default = { }; 56 | apply = default: { _file = "testing.common"; } // default; 57 | }; 58 | }; 59 | })); 60 | default = [ ]; 61 | }; 62 | 63 | tests = mkOption { 64 | description = "List of test cases"; 65 | default = [ ]; 66 | type = types.listOf (types.coercedTo types.path 67 | (module: { 68 | inherit module; 69 | }) 70 | (types.submodule testModule)); 71 | apply = tests: filter isTestEnabled tests; 72 | }; 73 | 74 | testsByName = mkOption { 75 | description = "Tests by name"; 76 | type = types.attrsOf types.attrs; 77 | default = listToAttrs (map (test: nameValuePair test.name test) cfg.tests); 78 | }; 79 | 80 | enabledTests = mkOption { 81 | description = "List of enabled tests (by default all tests are enabled)"; 82 | type = types.nullOr (types.listOf types.str); 83 | default = null; 84 | }; 85 | 86 | args = mkOption { 87 | description = "Attribute set of extra args passed to tests"; 88 | type = types.attrs; 89 | default = { }; 90 | }; 91 | 92 | success = mkOption { 93 | internal = true; # read only property 94 | description = "Whether testing was a success"; 95 | type = types.bool; 96 | default = all (test: test.success) cfg.tests; 97 | }; 98 | 99 | testScript = mkOption { 100 | internal = true; # set by test driver 101 | type = types.package; 102 | description = "Script to run e2e tests"; 103 | }; 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /tests/k8s/crd.nix: -------------------------------------------------------------------------------- 1 | { config, lib, kubenix, pkgs, ... }: 2 | 3 | with lib; 4 | let 5 | latestCrontab = config.kubernetes.api.resources.cronTabs.latest; 6 | in 7 | { 8 | imports = with kubenix.modules; [ test k8s ]; 9 | 10 | test = { 11 | name = "k8s-crd"; 12 | description = "Simple test tesing CRD"; 13 | enable = builtins.compareVersions config.kubernetes.version "1.8" >= 0; 14 | assertions = [{ 15 | message = "Custom resource should have correct version set"; 16 | assertion = latestCrontab.apiVersion == "stable.example.com/v2"; 17 | }]; 18 | script = '' 19 | @pytest.mark.applymanifest('${config.kubernetes.resultYAML}') 20 | def test_testing_module(kube): 21 | """Tests whether deployment gets successfully created""" 22 | 23 | kube.wait_for_registered(timeout=30) 24 | 25 | kube.get_crds() 26 | crds = kube.get_crds() 27 | crontabs_crd = crds.get('crontabs') 28 | assert contrabs_crd is not None 29 | 30 | # TODO: verify 31 | # kubectl get crontabs | grep -i versioned 32 | crontabs_crd_versioned = crontabs_crd.get('versioned') 33 | assert crontabs_crd_versioned is not None 34 | # kubectl get crontabs | grep -i latest 35 | crontabs_crd_latest = crontabs_crd.get('latest') 36 | assert crontabs_crd_latest is not None 37 | ''; 38 | }; 39 | 40 | kubernetes.customTypes = [ 41 | { 42 | group = "stable.example.com"; 43 | version = "v1"; 44 | kind = "CronTab"; 45 | attrName = "cronTabs"; 46 | description = "CronTabs resources"; 47 | module = { 48 | options.schedule = mkOption { 49 | description = "Crontab schedule script"; 50 | type = types.str; 51 | }; 52 | }; 53 | 54 | } 55 | { 56 | group = "stable.example.com"; 57 | version = "v2"; 58 | kind = "CronTab"; 59 | description = "CronTabs resources"; 60 | attrName = "cronTabs"; 61 | module = { 62 | options = { 63 | schedule = mkOption { 64 | description = "Crontab schedule script"; 65 | type = types.str; 66 | }; 67 | 68 | command = mkOption { 69 | description = "Command to run"; 70 | type = types.str; 71 | }; 72 | }; 73 | }; 74 | } 75 | { 76 | group = "stable.example.com"; 77 | version = "v3"; 78 | kind = "CronTab"; 79 | description = "CronTabs resources"; 80 | attrName = "cronTabsV3"; 81 | module = { 82 | options = { 83 | schedule = mkOption { 84 | description = "Crontab schedule script"; 85 | type = types.str; 86 | }; 87 | 88 | command = mkOption { 89 | description = "Command to run"; 90 | type = types.str; 91 | }; 92 | }; 93 | }; 94 | } 95 | ]; 96 | 97 | kubernetes.resources."stable.example.com"."v1".CronTab.versioned.spec.schedule = "* * * * *"; 98 | kubernetes.resources.cronTabs.latest.spec.schedule = "* * * * *"; 99 | kubernetes.resources.cronTabsV3.latest.spec.schedule = "* * * * *"; 100 | } 101 | -------------------------------------------------------------------------------- /tests/k8s/deployment.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, kubenix, images, test, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.kubernetes.api.resources.deployments.nginx; 6 | image = images.nginx; 7 | 8 | clientPod = builtins.toFile "client.json" (builtins.toJSON { 9 | apiVersion = "v1"; 10 | kind = "Pod"; 11 | metadata = { 12 | namespace = config.kubernetes.namespace; 13 | name = "curl"; 14 | }; 15 | spec.containers = [{ 16 | name = "curl"; 17 | image = config.docker.images.curl.path; 18 | args = [ "curl" "--retry" "20" "--retry-connrefused" "http://nginx" ]; 19 | }]; 20 | spec.restartPolicy = "Never"; 21 | }); 22 | 23 | in 24 | { 25 | imports = [ kubenix.modules.test kubenix.modules.k8s kubenix.modules.docker ]; 26 | 27 | test = { 28 | name = "k8s-deployment"; 29 | description = "Simple k8s testing a simple deployment"; 30 | assertions = [{ 31 | message = "should have correct apiVersion and kind set"; 32 | assertion = 33 | if ((builtins.compareVersions config.kubernetes.version "1.7") <= 0) 34 | then cfg.apiVersion == "apps/v1beta1" 35 | else if ((builtins.compareVersions config.kubernetes.version "1.8") <= 0) 36 | then cfg.apiVersion == "apps/v1beta2" 37 | else cfg.apiVersion == "apps/v1"; 38 | } 39 | { 40 | message = "should have corrent kind set"; 41 | assertion = cfg.kind == "Deployment"; 42 | } 43 | { 44 | message = "should have replicas set"; 45 | assertion = cfg.spec.replicas == 3; 46 | }]; 47 | script = '' 48 | import time 49 | 50 | @pytest.mark.applymanifest('${test.kubernetes.resultYAML}') 51 | def test_deployment(kube): 52 | """Tests whether deployment gets successfully created""" 53 | 54 | kube.wait_for_registered(timeout=30) 55 | 56 | deployments = kube.get_deployments() 57 | nginx_deploy = deployments.get('nginx') 58 | assert nginx_deploy is not None 59 | 60 | pods = nginx_deploy.get_pods() 61 | assert len(pods) == 3 62 | 63 | client_pod = kube.load_pod('${clientPod}') 64 | client_pod.create() 65 | 66 | client_pod.wait_until_ready(timeout=30) 67 | client_pod.wait_until_containers_start() 68 | 69 | container = client_pod.get_container('curl') 70 | 71 | time.sleep(5) 72 | 73 | logs = container.get_logs() 74 | 75 | assert "Hello from NGINX" in logs 76 | ''; 77 | }; 78 | 79 | docker.images = { 80 | nginx.image = image; 81 | curl.image = images.curl; 82 | }; 83 | 84 | kubernetes.resources.deployments.nginx = { 85 | spec = { 86 | replicas = 3; 87 | selector.matchLabels.app = "nginx"; 88 | template.metadata.labels.app = "nginx"; 89 | template.spec = { 90 | containers.nginx = { 91 | image = config.docker.images.nginx.path; 92 | imagePullPolicy = "IfNotPresent"; 93 | }; 94 | }; 95 | }; 96 | }; 97 | 98 | kubernetes.resources.services.nginx = { 99 | spec = { 100 | ports = [{ 101 | name = "http"; 102 | port = 80; 103 | }]; 104 | selector.app = "nginx"; 105 | }; 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /modules/testing/evalTest.nix: -------------------------------------------------------------------------------- 1 | { lib, config, testing, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | modules = [ 6 | # testing module 7 | config.module 8 | 9 | ./test-options.nix 10 | ../base.nix 11 | 12 | # passthru some options to test 13 | { 14 | config = { 15 | kubenix.project = mkDefault config.name; 16 | _module.args = { 17 | inherit kubenix; 18 | test = evaled.config; 19 | } // testing.args; 20 | }; 21 | } 22 | ]; 23 | 24 | # eval without checking 25 | evaled' = kubenix.evalModules { 26 | check = false; 27 | inherit modules; 28 | }; 29 | 30 | # test configuration 31 | testConfig = evaled'.config.test; 32 | 33 | # test features 34 | testFeatures = evaled'.config._m.features; 35 | 36 | # common options that can be applied on this test 37 | commonOpts = 38 | filter 39 | (d: 40 | (intersectLists d.features testFeatures) == d.features || 41 | (length d.features) == 0 42 | ) 43 | testing.common; 44 | 45 | # add common options modules to all modules 46 | modulesWithCommonOptions = modules ++ (map (d: d.options) commonOpts); 47 | 48 | # evaled test 49 | evaled = 50 | let 51 | evaled' = kubenix.evalModules { 52 | modules = modulesWithCommonOptions; 53 | }; 54 | in 55 | if testing.doThrowError then evaled' 56 | else if (builtins.tryEval evaled'.config.test.assertions).success 57 | then evaled' else null; 58 | 59 | in 60 | { 61 | options = { 62 | module = mkOption { 63 | description = "Module defining kubenix test"; 64 | type = types.unspecified; 65 | }; 66 | 67 | evaled = mkOption { 68 | description = "Test evaulation result"; 69 | type = types.nullOr types.attrs; 70 | internal = true; 71 | }; 72 | 73 | success = mkOption { 74 | description = "Whether test assertions were successfull"; 75 | type = types.bool; 76 | internal = true; 77 | default = false; 78 | }; 79 | 80 | # transparently forwarded from the test's `test` attribute for ease of access 81 | name = mkOption { 82 | description = "test name"; 83 | type = types.str; 84 | internal = true; 85 | }; 86 | 87 | description = mkOption { 88 | description = "test description"; 89 | type = types.str; 90 | internal = true; 91 | }; 92 | 93 | enable = mkOption { 94 | description = "Whether to enable test"; 95 | type = types.bool; 96 | internal = true; 97 | }; 98 | 99 | assertions = mkOption { 100 | description = "Test result"; 101 | type = types.unspecified; 102 | internal = true; 103 | default = [ ]; 104 | }; 105 | 106 | script = mkOption { 107 | description = "Test script to use for e2e test"; 108 | type = types.nullOr (types.either types.lines types.path); 109 | internal = true; 110 | }; 111 | 112 | }; 113 | 114 | config = mkMerge [ 115 | { 116 | inherit evaled; 117 | inherit (testConfig) name description enable; 118 | } 119 | 120 | # if test is evaled check assertions 121 | (mkIf (config.evaled != null) { 122 | inherit (evaled.config.test) assertions script; 123 | 124 | # if all assertions are true, test is successfull 125 | success = all (el: el.assertion) config.assertions; 126 | }) 127 | ]; 128 | } 129 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Kubernetes resource builder using nix"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | nixpkgs.url = "github:NixOS/nixpkgs"; 7 | devshell-flake.url = "github:numtide/devshell"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils, devshell-flake }: 11 | 12 | (flake-utils.lib.eachDefaultSystem (system: 13 | let 14 | 15 | pkgs = import nixpkgs { 16 | inherit system; 17 | overlays = [ 18 | self.overlay 19 | devshell-flake.overlay 20 | ]; 21 | config = { allowUnsupportedSystem = true; }; 22 | }; 23 | 24 | lib = pkgs.lib; 25 | 26 | kubenix = { 27 | lib = import ./lib { inherit lib pkgs; }; 28 | evalModules = self.evalModules.${system}; 29 | modules = self.modules; 30 | }; 31 | 32 | # evalModules with same interface as lib.evalModules and kubenix as 33 | # special argument 34 | evalModules = attrs@{ module ? null, modules ? [ module ], ... }: 35 | let 36 | lib' = lib.extend (lib: self: import ./lib/upstreamables.nix { inherit lib pkgs; }); 37 | attrs' = builtins.removeAttrs attrs [ "module" ]; 38 | in 39 | lib'.evalModules (lib.recursiveUpdate 40 | { 41 | inherit modules; 42 | specialArgs = { inherit kubenix; }; 43 | args = { 44 | inherit pkgs; 45 | name = "default"; 46 | }; 47 | } 48 | attrs'); 49 | 50 | in 51 | { 52 | 53 | inherit evalModules; 54 | 55 | jobs = import ./jobs { inherit pkgs; }; 56 | 57 | devShell = with pkgs; devshell.mkShell 58 | { imports = [ (devshell.importTOML ./devshell.toml) ]; }; 59 | 60 | packages = flake-utils.lib.flattenTree { 61 | inherit (pkgs) kubernetes kubectl; 62 | }; 63 | 64 | checks = let 65 | wasSuccess = suite: 66 | if suite.success == true 67 | then pkgs.runCommandNoCC "testing-suite-config-assertions-for-${suite.name}-succeeded" {} "echo success > $out" 68 | else pkgs.runCommandNoCC "testing-suite-config-assertions-for-${suite.name}-failed" {} "exit 1"; 69 | mkExamples = attrs: (import ./examples { inherit evalModules; }) 70 | ({ registry = "docker.io/gatehub"; } // attrs); 71 | mkK8STests = attrs: (import ./tests { inherit evalModules; }) 72 | ({ registry = "docker.io/gatehub"; } // attrs); 73 | in { 74 | # TODO: access "success" derivation with nice testing utils for nice output 75 | nginx-example = wasSuccess (mkExamples { }).nginx-deployment.config.testing; 76 | tests-k8s-1_19 = wasSuccess (mkK8STests { k8sVersion = "1.19"; }); 77 | tests-k8s-1_20 = wasSuccess (mkK8STests { k8sVersion = "1.20"; }); 78 | tests-k8s-1_21 = wasSuccess (mkK8STests { k8sVersion = "1.21"; }); 79 | }; 80 | 81 | } 82 | )) 83 | 84 | // 85 | 86 | { 87 | modules = import ./modules; 88 | overlay = final: prev: { 89 | kubenix.evalModules = self.evalModules.${prev.system}; 90 | # up to date versions of their nixpkgs equivalents 91 | kubernetes = prev.callPackage ./pkgs/applications/networking/cluster/kubernetes 92 | { }; 93 | kubectl = prev.callPackage ./pkgs/applications/networking/cluster/kubectl { }; 94 | }; 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /pkgs/applications/networking/cluster/kubernetes/mk-docker-opts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2014 The Kubernetes Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Generate Docker daemon options based on flannel env file. 18 | 19 | # exit on any error 20 | set -e 21 | 22 | usage() { 23 | echo "$0 [-f FLANNEL-ENV-FILE] [-d DOCKER-ENV-FILE] [-i] [-c] [-m] [-k COMBINED-KEY] 24 | 25 | Generate Docker daemon options based on flannel env file 26 | OPTIONS: 27 | -f Path to flannel env file. Defaults to /run/flannel/subnet.env 28 | -d Path to Docker env file to write to. Defaults to /run/docker_opts.env 29 | -i Output each Docker option as individual var. e.g. DOCKER_OPT_MTU=1500 30 | -c Output combined Docker options into DOCKER_OPTS var 31 | -k Set the combined options key to this value (default DOCKER_OPTS=) 32 | -m Do not output --ip-masq (useful for older Docker version) 33 | " >/dev/stderr 34 | exit 1 35 | } 36 | 37 | flannel_env="/run/flannel/subnet.env" 38 | docker_env="/run/docker_opts.env" 39 | combined_opts_key="DOCKER_OPTS" 40 | indiv_opts=false 41 | combined_opts=false 42 | ipmasq=true 43 | val="" 44 | 45 | while getopts "f:d:icmk:" opt; do 46 | case $opt in 47 | f) 48 | flannel_env=$OPTARG 49 | ;; 50 | d) 51 | docker_env=$OPTARG 52 | ;; 53 | i) 54 | indiv_opts=true 55 | ;; 56 | c) 57 | combined_opts=true 58 | ;; 59 | m) 60 | ipmasq=false 61 | ;; 62 | k) 63 | combined_opts_key=$OPTARG 64 | ;; 65 | \?) 66 | usage 67 | ;; 68 | esac 69 | done 70 | 71 | if [[ $indiv_opts = false ]] && [[ $combined_opts = false ]]; then 72 | indiv_opts=true 73 | combined_opts=true 74 | fi 75 | 76 | if [[ -f "${flannel_env}" ]]; then 77 | source "${flannel_env}" 78 | fi 79 | 80 | if [[ -n "$FLANNEL_SUBNET" ]]; then 81 | # shellcheck disable=SC2034 # Variable name referenced in OPT_LOOP below 82 | DOCKER_OPT_BIP="--bip=$FLANNEL_SUBNET" 83 | fi 84 | 85 | if [[ -n "$FLANNEL_MTU" ]]; then 86 | # shellcheck disable=SC2034 # Variable name referenced in OPT_LOOP below 87 | DOCKER_OPT_MTU="--mtu=$FLANNEL_MTU" 88 | fi 89 | 90 | if [[ "$FLANNEL_IPMASQ" = true ]] && [[ $ipmasq = true ]]; then 91 | # shellcheck disable=SC2034 # Variable name referenced in OPT_LOOP below 92 | DOCKER_OPT_IPMASQ="--ip-masq=false" 93 | fi 94 | 95 | eval docker_opts="\$${combined_opts_key}" 96 | docker_opts+=" " 97 | 98 | echo -n "" >"${docker_env}" 99 | 100 | # OPT_LOOP 101 | for opt in $(compgen -v DOCKER_OPT_); do 102 | eval val=\$"${opt}" 103 | 104 | if [[ "$indiv_opts" = true ]]; then 105 | echo "$opt=\"$val\"" >>"${docker_env}" 106 | fi 107 | 108 | docker_opts+="$val " 109 | done 110 | 111 | if [[ "$combined_opts" = true ]]; then 112 | echo "${combined_opts_key}=\"${docker_opts}\"" >>"${docker_env}" 113 | fi 114 | -------------------------------------------------------------------------------- /modules/helm.nix: -------------------------------------------------------------------------------- 1 | # helm defines kubenix module with options for using helm charts 2 | # with kubenix 3 | 4 | { config, lib, pkgs, helm, ... }: 5 | 6 | with lib; 7 | let 8 | cfg = config.kubernetes.helm; 9 | 10 | globalConfig = config; 11 | 12 | recursiveAttrs = mkOptionType { 13 | name = "recursive-attrs"; 14 | description = "recursive attribute set"; 15 | check = isAttrs; 16 | merge = loc: foldl' (res: def: recursiveUpdate res def.value) { }; 17 | }; 18 | 19 | parseApiVersion = apiVersion: 20 | let 21 | splitted = splitString "/" apiVersion; 22 | in 23 | { 24 | group = if length splitted == 1 then "core" else head splitted; 25 | version = last splitted; 26 | }; 27 | 28 | in 29 | { 30 | imports = [ ./k8s.nix ]; 31 | 32 | options.kubernetes.helm = { 33 | instances = mkOption { 34 | description = "Attribute set of helm instances"; 35 | type = types.attrsOf (types.submodule ({ config, name, ... }: { 36 | options = { 37 | name = mkOption { 38 | description = "Helm release name"; 39 | type = types.str; 40 | default = name; 41 | }; 42 | 43 | chart = mkOption { 44 | description = "Helm chart to use"; 45 | type = types.package; 46 | }; 47 | 48 | namespace = mkOption { 49 | description = "Namespace to install helm chart to"; 50 | type = types.nullOr types.str; 51 | default = null; 52 | }; 53 | 54 | values = mkOption { 55 | description = "Values to pass to chart"; 56 | type = recursiveAttrs; 57 | default = { }; 58 | }; 59 | 60 | kubeVersion = mkOption { 61 | description = "Kubernetes version to build chart for"; 62 | type = types.str; 63 | default = globalConfig.kubernetes.version; 64 | }; 65 | 66 | overrides = mkOption { 67 | description = "Overrides to apply to all chart resources"; 68 | type = types.listOf types.unspecified; 69 | default = [ ]; 70 | }; 71 | 72 | overrideNamespace = mkOption { 73 | description = "Whether to apply namespace override"; 74 | type = types.bool; 75 | default = true; 76 | }; 77 | 78 | objects = mkOption { 79 | description = "Generated kubernetes objects"; 80 | type = types.listOf types.attrs; 81 | default = [ ]; 82 | }; 83 | }; 84 | 85 | config.overrides = mkIf (config.overrideNamespace && config.namespace != null) [{ 86 | metadata.namespace = config.namespace; 87 | }]; 88 | 89 | config.objects = importJSON (helm.chart2json { 90 | inherit (config) chart name namespace values kubeVersion; 91 | }); 92 | })); 93 | default = { }; 94 | }; 95 | }; 96 | 97 | config = { 98 | # expose helm helper methods as module argument 99 | _module.args.helm = import ../lib/helm { inherit pkgs; }; 100 | 101 | kubernetes.api.resources = mkMerge (flatten (mapAttrsToList 102 | (_: instance: 103 | map 104 | (object: 105 | let 106 | apiVersion = parseApiVersion object.apiVersion; 107 | name = object.metadata.name; 108 | in 109 | { 110 | "${apiVersion.group}"."${apiVersion.version}".${object.kind}."${name}" = mkMerge ([ 111 | object 112 | ] ++ instance.overrides); 113 | }) 114 | instance.objects 115 | ) 116 | cfg.instances)); 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /tests/helm/simple.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, kubenix, helm, ... }: 2 | 3 | with lib; 4 | with kubenix.lib; 5 | with pkgs.dockerTools; 6 | let 7 | corev1 = config.kubernetes.api.resources.core.v1; 8 | appsv1 = config.kubernetes.api.resources.apps.v1; 9 | 10 | postgresql = pullImage { 11 | imageName = "docker.io/bitnami/postgresql"; 12 | imageDigest = "sha256:ec16eb9ff2e7bf0669cfc52e595f17d9c52efd864c3f943f404d525dafaaaf96"; 13 | sha256 = "12jr8pzvj1qglrp7sh857a5mv4nda67hw9si6ah2bl6y1ff19l65"; 14 | finalImageName = "docker.io/bitnami/postgresql"; 15 | finalImageTag = "11.7.0-debian-10-r55"; 16 | }; 17 | 18 | postgresqlExporter = pullImage { 19 | imageName = "docker.io/bitnami/postgres-exporter"; 20 | imageDigest = "sha256:373ba8ac1892291b4121591d1d933b7f9501ae45b9b8d570d7deb4900f91cfe9"; 21 | sha256 = "0icrqmlj8127jhmiy3vh419cv7hwnw19xdn89hxxyj2l6a1chryh"; 22 | finalImageName = "docker.io/bitnami/postgres-exporter"; 23 | finalImageTag = "0.9.0-debian-10-r43"; 24 | }; 25 | 26 | bitnamiShell = pullImage { 27 | imageName = "docker.io/bitnami/bitnami-shell"; 28 | imageDigest = "sha256:58ba68e1f1d9a55c1234ae5b439bdcf0de1931e4aa1bac7bd0851b66de14fd97"; 29 | sha256 = "00lmphm3ds17apbmh2m2r7cz05jhp4dc3ynswrj0pbpq0azif4zn"; 30 | finalImageName = "docker.io/bitnami/bitnami-shell"; 31 | finalImageTag = "10"; 32 | }; 33 | in 34 | { 35 | imports = [ kubenix.modules.test kubenix.modules.helm kubenix.modules.k8s kubenix.modules.docker ]; 36 | 37 | docker.images = { 38 | postgresql.image = inherit postgresql; 39 | postgresqlExporter.image = inherit postgresqlExporter; 40 | bitnamiShell.image = inherit bitnamiShell; 41 | }; 42 | 43 | test = { 44 | name = "helm-simple"; 45 | description = "Simple k8s testing wheter name, apiVersion and kind are preset"; 46 | assertions = [{ 47 | message = "should have generated resources"; 48 | assertion = 49 | appsv1.StatefulSet ? "app-psql-postgresql-primary" && 50 | appsv1.StatefulSet ? "app-psql-postgresql-read" && 51 | corev1.Secret ? "app-psql-postgresql" && 52 | corev1.Service ? "app-psql-postgresql-headless"; 53 | } 54 | { 55 | message = "should have values passed"; 56 | assertion = appsv1.StatefulSet.app-psql-postgresql-read.spec.replicas == 2; 57 | } 58 | { 59 | message = "should have namespace defined"; 60 | assertion = 61 | appsv1.StatefulSet.app-psql-postgresql-primary.metadata.namespace == "test"; 62 | }]; 63 | script = '' 64 | @pytest.mark.applymanifest('${config.kubernetes.resultYAML}') 65 | def test_helm_deployment(kube): 66 | """Tests whether helm deployment gets successfully created""" 67 | 68 | kube.wait_for_registered(timeout=30) 69 | 70 | # TODO: implement those kind of checks from the host machine into the cluster 71 | # via port forwarding, prepare all runtimes accordingly 72 | # PGPASSWORD=postgres ${pkgs.postgresql}/bin/psql -h app-psql-postgresql.test.svc.cluster.local -U postgres -l 73 | ''; 74 | }; 75 | 76 | kubernetes.helm.instances.app-psql = { 77 | namespace = "some-overridden-by-kubetest"; 78 | chart = helm.fetch { 79 | repo = "https://charts.bitnami.com/bitnami"; 80 | chart = "postgresql"; 81 | version = "10.3.8"; 82 | sha256 = "sha256-0hJ5pNIivpXeRal1DwJ2VSD3Yxtw2omOoIYGZKGtu9I="; 83 | }; 84 | 85 | values = { 86 | image = { 87 | repository = "bitnami/postgresql"; 88 | tag = "11.11.0-debian-10-r71"; 89 | pullPolicy = "IfNotPresent"; 90 | }; 91 | volumePermissions.image = { 92 | repository = "bitnami/bitnami-shell"; 93 | tag = "10"; 94 | pullPolicy = "IfNotPresent"; 95 | }; 96 | metrics.image = { 97 | repository = "bitnami/postgres-exporter"; 98 | tag = "0.9.0-debian-10-r43"; 99 | pullPolicy = "IfNotPresent"; 100 | }; 101 | replication.enabled = true; 102 | replication.slaveReplicas = 2; 103 | postgresqlPassword = "postgres"; 104 | persistence.enabled = false; 105 | }; 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /tests/submodules/defaults.nix: -------------------------------------------------------------------------------- 1 | { name, config, lib, kubenix, ... }: 2 | 3 | with lib; 4 | let 5 | instance1 = config.submodules.instances.instance1; 6 | instance2 = config.submodules.instances.instance2; 7 | instance3 = config.submodules.instances.instance3; 8 | instance4 = config.submodules.instances.instance4; 9 | instance5 = config.submodules.instances.instance5; 10 | versioned-submodule = config.submodules.instances.versioned-submodule; 11 | 12 | submodule = { name, ... }: { 13 | imports = [ kubenix.modules.submodule ]; 14 | 15 | options.submodule.args = { 16 | value = mkOption { 17 | description = "Submodule value"; 18 | type = types.str; 19 | }; 20 | 21 | defaultValue = mkOption { 22 | description = "Submodule default value"; 23 | type = types.str; 24 | }; 25 | }; 26 | }; 27 | in 28 | { 29 | imports = with kubenix.modules; [ test submodules ]; 30 | 31 | test = { 32 | name = "submodules-defaults"; 33 | description = "Simple submodule test"; 34 | assertions = [{ 35 | message = "should apply defaults by tag1"; 36 | assertion = instance1.config.submodule.args.value == "value1"; 37 | } 38 | { 39 | message = "should apply defaults by tag2"; 40 | assertion = instance2.config.submodule.args.value == "value2"; 41 | } 42 | { 43 | message = "should apply defaults by tag2"; 44 | assertion = instance3.config.submodule.args.value == "value2"; 45 | } 46 | { 47 | message = "should apply defaults to all"; 48 | assertion = 49 | instance1.config.submodule.args.defaultValue == "value" && 50 | instance2.config.submodule.args.defaultValue == "value"; 51 | } 52 | { 53 | message = "instance1 and instance3 should have value of default-value"; 54 | assertion = instance3.config.submodule.args.defaultValue == "default-value"; 55 | } 56 | { 57 | message = "should apply defaults by submodule name"; 58 | assertion = instance4.config.submodule.args.value == "value4"; 59 | } 60 | { 61 | message = "should apply defaults by custom condition"; 62 | assertion = instance5.config.submodule.args.defaultValue == "my-custom-value"; 63 | } 64 | { 65 | message = "should apply defaults to versioned submodule"; 66 | assertion = versioned-submodule.config.submodule.args.defaultValue == "versioned-submodule"; 67 | }]; 68 | }; 69 | 70 | submodules.imports = [{ 71 | modules = [ 72 | submodule 73 | { 74 | submodule = { 75 | name = "submodule1"; 76 | tags = [ "tag1" ]; 77 | }; 78 | } 79 | ]; 80 | } 81 | { 82 | modules = [ 83 | submodule 84 | { 85 | submodule = { 86 | name = "submodule2"; 87 | tags = [ "tag2" ]; 88 | }; 89 | } 90 | ]; 91 | } 92 | { 93 | modules = [ 94 | submodule 95 | { 96 | submodule = { 97 | name = "submodule3"; 98 | tags = [ "tag2" ]; 99 | }; 100 | } 101 | ]; 102 | } 103 | { 104 | modules = [ 105 | submodule 106 | { 107 | submodule = { 108 | name = "submodule4"; 109 | }; 110 | } 111 | ]; 112 | } 113 | { 114 | modules = [ 115 | submodule 116 | { 117 | submodule = { 118 | name = "submodule5"; 119 | }; 120 | submodule.args.value = "custom-value"; 121 | } 122 | ]; 123 | } 124 | { 125 | modules = [ 126 | submodule 127 | { 128 | submodule = { 129 | name = "versioned-submodule"; 130 | version = "2.0.0"; 131 | }; 132 | } 133 | ]; 134 | }]; 135 | 136 | submodules.defaults = [{ 137 | default.submodule.args.defaultValue = mkDefault "value"; 138 | } 139 | { 140 | tags = [ "tag1" ]; 141 | default.submodule.args.value = mkDefault "value1"; 142 | } 143 | { 144 | tags = [ "tag2" ]; 145 | default.submodule.args.value = mkDefault "value2"; 146 | } 147 | { 148 | name = "submodule4"; 149 | default.submodule.args.value = mkDefault "value4"; 150 | } 151 | { 152 | default = { config, ... }: { 153 | submodule.args.defaultValue = mkIf (config.submodule.args.value == "custom-value") "my-custom-value"; 154 | }; 155 | } 156 | { 157 | name = "versioned-submodule"; 158 | version = "2.0.0"; 159 | default.submodule.args.value = mkDefault "versioned"; 160 | }]; 161 | 162 | submodules.instances.instance1.submodule = "submodule1"; 163 | submodules.instances.instance2.submodule = "submodule2"; 164 | submodules.instances.instance3 = { 165 | submodule = "submodule3"; 166 | args.defaultValue = "default-value"; 167 | }; 168 | submodules.instances.instance4.submodule = "submodule4"; 169 | submodules.instances.instance5.submodule = "submodule5"; 170 | submodules.instances.versioned-submodule = { 171 | submodule = "versioned-submodule"; 172 | args.defaultValue = "versioned-submodule"; 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /modules/submodules.nix: -------------------------------------------------------------------------------- 1 | { config, options, kubenix, pkgs, lib, ... }: 2 | 3 | with lib; 4 | let 5 | cfg = config.submodules; 6 | parentConfig = config; 7 | 8 | matchesVersion = requiredVersion: version: 9 | if requiredVersion != null then 10 | if hasPrefix "~" requiredVersion 11 | then (builtins.match (removePrefix "~" requiredVersion) version) != null 12 | else requiredVersion == version 13 | else true; 14 | 15 | getDefaults = { name, version, tags, features }: 16 | catAttrs "default" (filter 17 | (submoduleDefault: 18 | (submoduleDefault.name == null || submoduleDefault.name == name) && 19 | (matchesVersion submoduleDefault.version version) && 20 | ( 21 | (length submoduleDefault.tags == 0) || 22 | (length (intersectLists submoduleDefault.tags tags)) > 0 23 | ) && 24 | ( 25 | (length submoduleDefault.features == 0) || 26 | (length (intersectLists submoduleDefault.features features)) > 0 27 | ) 28 | ) 29 | config.submodules.defaults); 30 | 31 | specialArgs = cfg.specialArgs // { 32 | parentConfig = config; 33 | }; 34 | 35 | findSubmodule = { name, version ? null, latest ? true }: 36 | let 37 | matchingSubmodules = filter 38 | (el: 39 | el.definition.name == name && 40 | (matchesVersion version el.definition.version) 41 | ) 42 | cfg.imports; 43 | 44 | versionSortedSubmodules = sort 45 | (s1: s2: 46 | if builtins.compareVersions s1.definition.version s2.definition.version > 0 47 | then true else false 48 | ) 49 | matchingSubmodules; 50 | 51 | matchingModule = 52 | if length versionSortedSubmodules == 0 53 | then throw "No module found ${name}/${if version == null then "latest" else version}" 54 | else head versionSortedSubmodules; 55 | in 56 | matchingModule; 57 | 58 | passthruConfig = mapAttrsToList 59 | (name: opt: { 60 | ${name} = mkMerge (mapAttrsToList 61 | (_: inst: 62 | if inst.passthru.enable 63 | then inst.config.submodule.passthru.${name} or { } 64 | else { } 65 | ) 66 | config.submodules.instances); 67 | 68 | _module.args = mkMerge (mapAttrsToList 69 | (_: inst: 70 | if inst.passthru.enable 71 | then inst.config.submodule.passthru._module.args or { } 72 | else { } 73 | ) 74 | config.submodules.instances); 75 | }) 76 | (removeAttrs options [ "_definedNames" "_module" "_m" "submodules" ]); 77 | 78 | submoduleWithSpecialArgs = opts: specialArgs: 79 | let 80 | opts' = toList opts; 81 | inherit (lib.modules) evalModules; 82 | in 83 | mkOptionType rec { 84 | name = "submodule"; 85 | check = x: isAttrs x || isFunction x; 86 | merge = loc: defs: 87 | let 88 | coerce = def: if isFunction def then def else { config = def; }; 89 | modules = opts' ++ map (def: { _file = def.file; imports = [ (coerce def.value) ]; }) defs; 90 | in 91 | (evalModules { 92 | inherit modules specialArgs; 93 | prefix = loc; 94 | }).config; 95 | getSubOptions = prefix: (evalModules 96 | { 97 | modules = opts'; inherit prefix specialArgs; 98 | # This is a work-around due to the fact that some sub-modules, 99 | # such as the one included in an attribute set, expects a "args" 100 | # attribute to be given to the sub-module. As the option 101 | # evaluation does not have any specific attribute name, we 102 | # provide a default one for the documentation. 103 | # 104 | # This is mandatory as some option declaration might use the 105 | # "name" attribute given as argument of the submodule and use it 106 | # as the default of option declarations. 107 | # 108 | # Using lookalike unicode single angle quotation marks because 109 | # of the docbook transformation the options receive. In all uses 110 | # > and < wouldn't be encoded correctly so the encoded values 111 | # would be used, and use of `<` and `>` would break the XML document. 112 | # It shouldn't cause an issue since this is cosmetic for the manual. 113 | args.name = "‹name›"; 114 | }).options; 115 | getSubModules = opts'; 116 | substSubModules = m: submoduleWithSpecialArgs m specialArgs; 117 | functor = (defaultFunctor name) // { 118 | # Merging of submodules is done as part of mergeOptionDecls, as we have to annotate 119 | # each submodule with its location. 120 | payload = [ ]; 121 | binOp = lhs: rhs: [ ]; 122 | }; 123 | }; 124 | in 125 | { 126 | imports = [ ./base.nix ]; 127 | 128 | options = { 129 | submodules.specialArgs = mkOption { 130 | description = "Special args to pass to submodules. These arguments can be used for imports"; 131 | type = types.attrs; 132 | default = { }; 133 | }; 134 | 135 | submodules.defaults = mkOption { 136 | description = "List of defaults to apply to submodule instances"; 137 | type = types.listOf (types.submodule ({ config, ... }: { 138 | options = { 139 | name = mkOption { 140 | description = "Name of the submodule to apply defaults for"; 141 | type = types.nullOr types.str; 142 | default = null; 143 | }; 144 | 145 | version = mkOption { 146 | description = '' 147 | Version of submodule to apply defaults for. If version starts with 148 | "~" it is threated as regex pattern for example "~1.0.* 149 | ''; 150 | type = types.nullOr types.str; 151 | default = null; 152 | }; 153 | 154 | tags = mkOption { 155 | description = "List of tags to apply defaults for"; 156 | type = types.listOf types.str; 157 | default = [ ]; 158 | }; 159 | 160 | features = mkOption { 161 | description = "List of features that submodule has to have to apply defaults"; 162 | type = types.listOf types.str; 163 | default = [ ]; 164 | }; 165 | 166 | default = mkOption { 167 | description = "Default to apply to submodule instance"; 168 | type = types.unspecified; 169 | default = { }; 170 | }; 171 | }; 172 | })); 173 | default = [ ]; 174 | }; 175 | 176 | submodules.propagate.enable = mkOption { 177 | description = "Whether to propagate defaults and imports from parent to child"; 178 | type = types.bool; 179 | default = true; 180 | }; 181 | 182 | submodules.imports = mkOption { 183 | description = "List of submodule imports"; 184 | type = types.listOf ( 185 | types.coercedTo 186 | types.path 187 | (module: { inherit module; }) 188 | (types.submodule ({ name, config, ... }: 189 | let 190 | evaledSubmodule' = evalModules { 191 | inherit specialArgs; 192 | modules = config.modules ++ [ ./base.nix ]; 193 | check = false; 194 | }; 195 | 196 | evaledSubmodule = 197 | if (!(elem "submodule" evaledSubmodule'.config._m.features)) 198 | then throw "no submodule defined" 199 | else evaledSubmodule'; 200 | in 201 | { 202 | options = { 203 | module = mkOption { 204 | description = "Module defining submodule"; 205 | type = types.unspecified; 206 | }; 207 | 208 | modules = mkOption { 209 | description = "List of modules defining submodule"; 210 | type = types.listOf types.unspecified; 211 | default = [ config.module ]; 212 | }; 213 | 214 | features = mkOption { 215 | description = "List of features exposed by submodule"; 216 | type = types.listOf types.str; 217 | }; 218 | 219 | definition = mkOption { 220 | description = "Submodule definition"; 221 | type = types.attrs; 222 | }; 223 | 224 | exportAs = mkOption { 225 | description = "Name under which to register exports"; 226 | type = types.nullOr types.str; 227 | default = null; 228 | }; 229 | }; 230 | 231 | config = { 232 | definition = { 233 | inherit (evaledSubmodule.config.submodule) name description version tags exports; 234 | }; 235 | 236 | features = evaledSubmodule.config._m.features; 237 | }; 238 | }) 239 | ) 240 | ); 241 | default = [ ]; 242 | }; 243 | 244 | submodules.instances = mkOption { 245 | description = "Attribute set of submodule instances"; 246 | default = { }; 247 | type = types.attrsOf (types.submodule ({ name, config, options, ... }: 248 | let 249 | # submodule associated with 250 | submodule = findSubmodule { 251 | name = config.submodule; 252 | version = config.version; 253 | }; 254 | 255 | # definition of a submodule 256 | submoduleDefinition = submodule.definition; 257 | 258 | # submodule defaults 259 | defaults = getDefaults { 260 | name = submoduleDefinition.name; 261 | version = submoduleDefinition.version; 262 | tags = submoduleDefinition.tags; 263 | features = submodule.features; 264 | }; 265 | in 266 | { 267 | options = { 268 | name = mkOption { 269 | description = "Submodule instance name"; 270 | type = types.str; 271 | default = name; 272 | }; 273 | 274 | submodule = mkOption { 275 | description = "Name of the submodule to use"; 276 | type = types.str; 277 | default = name; 278 | }; 279 | 280 | version = mkOption { 281 | description = '' 282 | Version of submodule to use, if version starts with "~" it is 283 | threated as regex pattern for example "~1.0.*" 284 | ''; 285 | type = types.nullOr types.str; 286 | default = null; 287 | }; 288 | 289 | passthru.enable = mkOption { 290 | description = "Whether to passthru submodule resources"; 291 | type = types.bool; 292 | default = true; 293 | }; 294 | 295 | config = mkOption { 296 | description = "Submodule instance ${config.name} for ${submoduleDefinition.name}:${submoduleDefinition.version} config"; 297 | type = submoduleWithSpecialArgs 298 | ({ ... }: { 299 | imports = submodule.modules ++ defaults ++ [ ./base.nix ]; 300 | _module.args.pkgs = pkgs; 301 | _module.args.name = config.name; 302 | _module.args.submodule = config; 303 | submodule.args = mkAliasDefinitions options.args; 304 | }) 305 | specialArgs; 306 | default = { }; 307 | }; 308 | 309 | args = mkOption { 310 | description = "Submodule arguments (alias of config.submodule.args)"; 311 | }; 312 | }; 313 | })); 314 | }; 315 | default = { }; 316 | }; 317 | 318 | config = mkMerge ([ 319 | { 320 | # register exported functions as args 321 | _module.args = mkMerge (map 322 | (submodule: { 323 | ${submodule.exportAs} = submodule.definition.exports; 324 | }) 325 | (filter (submodule: submodule.exportAs != null) cfg.imports)); 326 | 327 | _m.features = [ "submodules" ]; 328 | 329 | submodules.specialArgs.kubenix = kubenix; 330 | 331 | # passthru kubenix.project to submodules 332 | submodules.defaults = mkMerge [ 333 | [{ 334 | default = { 335 | kubenix.project = parentConfig.kubenix.project; 336 | }; 337 | }] 338 | 339 | (map 340 | (propagate: { 341 | features = propagate.features; 342 | default = propagate.module; 343 | }) 344 | config._m.propagate) 345 | ]; 346 | } 347 | 348 | (mkIf cfg.propagate.enable { 349 | # if propagate is enabled and submodule has submodules included propagage defaults and imports 350 | submodules.defaults = [{ 351 | features = [ "submodules" ]; 352 | default = { 353 | submodules = { 354 | defaults = cfg.defaults; 355 | imports = cfg.imports; 356 | }; 357 | }; 358 | }]; 359 | }) 360 | ] ++ passthruConfig); 361 | } 362 | -------------------------------------------------------------------------------- /jobs/generators/istio/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { }, lib ? pkgs.lib, spec ? ./istio-schema.json }: 2 | 3 | with lib; 4 | let 5 | gen = rec { 6 | mkMerge = values: ''mkMerge [${concatMapStrings 7 | (value: " 8 | ${value} 9 | ") 10 | values}]''; 11 | 12 | toNixString = value: 13 | if isAttrs value || isList value 14 | then builtins.toJSON value 15 | else if isString value 16 | then ''"${value}"'' 17 | else if value == null 18 | then "null" 19 | else builtins.toString value; 20 | 21 | removeEmptyLines = str: concatStringsSep "\n" (filter (l: (builtins.match "( |)+" l) == null) (splitString "\n" str)); 22 | 23 | mkOption = 24 | { description ? null 25 | , type ? null 26 | , default ? null 27 | , apply ? null 28 | }: removeEmptyLines ''mkOption { 29 | ${optionalString (description != null) "description = ${builtins.toJSON description};"} 30 | ${optionalString (type != null) ''type = ${type};''} 31 | ${optionalString (default != null) ''default = ${toNixString default};''} 32 | ${optionalString (apply != null) ''apply = ${apply};''} 33 | }''; 34 | 35 | mkOverride = priority: value: "mkOverride ${toString priority} ${toNixString value}"; 36 | 37 | types = { 38 | unspecified = "types.unspecified"; 39 | str = "types.str"; 40 | int = "types.int"; 41 | bool = "types.bool"; 42 | attrs = "types.attrs"; 43 | nullOr = val: "(types.nullOr ${val})"; 44 | attrsOf = val: "(types.attrsOf ${val})"; 45 | listOf = val: "(types.listOf ${val})"; 46 | coercedTo = coercedType: coerceFunc: finalType: 47 | "(types.coercedTo ${coercedType} ${coerceFunc} ${finalType})"; 48 | either = val1: val2: "(types.either ${val1} ${val2})"; 49 | loaOf = type: "(types.loaOf ${type})"; 50 | }; 51 | 52 | hasTypeMapping = def: 53 | hasAttr "type" def && 54 | elem def.type [ "string" "integer" "boolean" ]; 55 | 56 | mergeValuesByKey = mergeKey: ''(mergeValuesByKey "${mergeKey}")''; 57 | 58 | mapType = def: 59 | if def.type == "string" then 60 | if hasAttr "format" def && def.format == "int-or-string" 61 | then types.either types.int types.str 62 | else types.str 63 | else if def.type == "integer" then types.int 64 | else if def.type == "number" then types.int 65 | else if def.type == "boolean" then types.bool 66 | else if def.type == "object" then types.attrs 67 | else throw "type ${def.type} not supported"; 68 | 69 | submoduleOf = definitions: ref: ''(submoduleOf "${ref}")''; 70 | 71 | submoduleForDefinition = ref: name: kind: group: version: 72 | ''(submoduleForDefinition "${ref}" "${name}" "${kind}" "${group}" "${version}")''; 73 | 74 | coerceAttrsOfSubmodulesToListByKey = ref: mergeKey: 75 | ''(coerceAttrsOfSubmodulesToListByKey "${ref}" "${mergeKey}")''; 76 | 77 | attrsToList = "values: if values != null then mapAttrsToList (n: v: v) values else values"; 78 | 79 | refDefinition = attr: head (tail (tail (splitString "/" attr."$ref"))); 80 | }; 81 | 82 | fixJSON = content: replaceStrings [ "\\u" ] [ "u" ] content; 83 | 84 | fetchSpecs = path: builtins.fromJSON (fixJSON (builtins.readFile path)); 85 | 86 | genDefinitions = swagger: with gen; (mapAttrs 87 | (name: definition: 88 | # if $ref is in definition it means it's an alias of other definition 89 | if hasAttr "$ref" definition 90 | then definitions."${refDefinition definition}" 91 | 92 | else if !(hasAttr "properties" definition) 93 | then { 94 | type = mapType definition; 95 | } 96 | 97 | else { 98 | options = mapAttrs 99 | (propName: property: 100 | let 101 | isRequired = elem propName (definition.required or [ ]); 102 | requiredOrNot = type: if isRequired then type else types.nullOr type; 103 | optionProperties = 104 | # if $ref is in property it references other definition, 105 | # but if other definition does not have properties, then just take it's type 106 | if hasAttr "$ref" property then 107 | if hasTypeMapping swagger.definitions.${refDefinition property} then { 108 | type = requiredOrNot (mapType swagger.definitions.${refDefinition property}); 109 | } 110 | else { 111 | type = requiredOrNot (submoduleOf definitions (refDefinition property)); 112 | } 113 | 114 | else if !(hasAttr "type" property) then { 115 | type = types.unspecified; 116 | } 117 | 118 | # if property has an array type 119 | else if property.type == "array" then 120 | 121 | # if reference is in items it can reference other type of another 122 | # definition 123 | if hasAttr "$ref" property.items then 124 | 125 | # if it is a reference to simple type 126 | if hasTypeMapping swagger.definitions.${refDefinition property.items} 127 | then { 128 | type = requiredOrNot (types.listOf (mapType swagger.definitions.${refDefinition property.items}.type)); 129 | } 130 | 131 | # if a reference is to complex type 132 | else 133 | # if x-kubernetes-patch-merge-key is set then make it an 134 | # attribute set of submodules 135 | if hasAttr "x-kubernetes-patch-merge-key" property 136 | then 137 | let 138 | mergeKey = property."x-kubernetes-patch-merge-key"; 139 | in 140 | { 141 | type = requiredOrNot (coerceAttrsOfSubmodulesToListByKey (refDefinition property.items) mergeKey); 142 | apply = attrsToList; 143 | } 144 | 145 | # in other case it's a simple list 146 | else { 147 | type = requiredOrNot (types.listOf (submoduleOf definitions (refDefinition property.items))); 148 | } 149 | 150 | # in other case it only references a simple type 151 | else { 152 | type = requiredOrNot (types.listOf (mapType property.items)); 153 | } 154 | 155 | else if property.type == "object" && hasAttr "additionalProperties" property 156 | then 157 | # if it is a reference to simple type 158 | if ( 159 | hasAttr "$ref" property.additionalProperties && 160 | hasTypeMapping swagger.definitions.${refDefinition property.additionalProperties} 161 | ) then { 162 | type = requiredOrNot (types.attrsOf (mapType swagger.definitions.${refDefinition property.additionalProperties})); 163 | } 164 | 165 | else if hasAttr "$ref" property.additionalProperties 166 | then { 167 | type = requiredOrNot types.attrs; 168 | } 169 | 170 | # if is an array 171 | else if property.additionalProperties.type == "array" 172 | then { 173 | type = requiredOrNot (types.loaOf (mapType property.additionalProperties.items)); 174 | } 175 | 176 | else { 177 | type = requiredOrNot (types.attrsOf (mapType property.additionalProperties)); 178 | } 179 | 180 | # just a simple property 181 | else { 182 | type = requiredOrNot (mapType property); 183 | }; 184 | in 185 | mkOption ({ 186 | description = property.description or ""; 187 | } // optionProperties) 188 | ) 189 | definition.properties; 190 | config = 191 | let 192 | optionalProps = filterAttrs 193 | (propName: property: 194 | !(elem propName (definition.required or [ ])) 195 | ) 196 | definition.properties; 197 | in 198 | mapAttrs (name: property: mkOverride 1002 null) optionalProps; 199 | } 200 | ) 201 | swagger.definitions); 202 | 203 | genResources = swagger: (mapAttrsToList 204 | (name: property: rec { 205 | splittedType = splitString "." (removePrefix "me.snowdrop.istio.api." property.javaType); 206 | group = (concatStringsSep "." (take ((length splittedType) - 2) splittedType)) + ".istio.io"; 207 | kind = removeSuffix "Spec" (last splittedType); 208 | version = last (take ((length splittedType) - 1) splittedType); 209 | ref = removePrefix "#/definitions/" property."$ref"; 210 | }) 211 | (filterAttrs 212 | (name: property: 213 | (hasPrefix "me.snowdrop.istio.api" property.javaType) && 214 | hasSuffix "Spec" property.javaType 215 | ) 216 | swagger.properties)) ++ (mapAttrsToList 217 | (name: property: rec { 218 | splittedType = splitString "." (removePrefix "me.snowdrop.istio.mixer." property.javaType); 219 | group = "config.istio.io"; 220 | version = "v1alpha2"; 221 | kind = head (tail splittedType); 222 | ref = removePrefix "#/definitions/" property."$ref"; 223 | }) 224 | (filterAttrs 225 | (name: property: 226 | (hasPrefix "me.snowdrop.istio.mixer" property.javaType) && 227 | hasSuffix "Spec" property.javaType 228 | ) 229 | swagger.properties)); 230 | 231 | swagger = fetchSpecs spec; 232 | 233 | definitions = genDefinitions swagger; 234 | 235 | generated = '' 236 | # This file was generated with kubenix k8s generator, do not edit 237 | {lib, config, ... }: 238 | 239 | with lib; 240 | 241 | let 242 | types = lib.types // rec { 243 | str = mkOptionType { 244 | name = "str"; 245 | description = "string"; 246 | check = isString; 247 | merge = mergeEqualOption; 248 | }; 249 | 250 | # Either value of type `finalType` or `coercedType`, the latter is 251 | # converted to `finalType` using `coerceFunc`. 252 | coercedTo = coercedType: coerceFunc: finalType: 253 | mkOptionType rec { 254 | name = "coercedTo"; 255 | description = "''${finalType.description} or ''${coercedType.description}"; 256 | check = x: finalType.check x || coercedType.check x; 257 | merge = loc: defs: 258 | let 259 | coerceVal = val: 260 | if finalType.check val then val 261 | else let 262 | coerced = coerceFunc val; 263 | in assert finalType.check coerced; coerced; 264 | 265 | in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 266 | getSubOptions = finalType.getSubOptions; 267 | getSubModules = finalType.getSubModules; 268 | substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 269 | typeMerge = t1: t2: null; 270 | functor = (defaultFunctor name) // { wrapped = finalType; }; 271 | }; 272 | }; 273 | 274 | mkOptionDefault = mkOverride 1001; 275 | 276 | extraOptions = { 277 | kubenix = {}; 278 | }; 279 | 280 | mergeValuesByKey = mergeKey: values: 281 | listToAttrs (map 282 | (value: nameValuePair ( 283 | if isAttrs value.''${mergeKey} 284 | then toString value.''${mergeKey}.content 285 | else (toString value.''${mergeKey}) 286 | ) value) 287 | values); 288 | 289 | submoduleOf = ref: types.submodule ({name, ...}: { 290 | options = definitions."''${ref}".options; 291 | config = definitions."''${ref}".config; 292 | }); 293 | 294 | submoduleWithMergeOf = ref: mergeKey: types.submodule ({name, ...}: let 295 | convertName = name: 296 | if definitions."''${ref}".options.''${mergeKey}.type == types.int 297 | then toInt name 298 | else name; 299 | in { 300 | options = definitions."''${ref}".options; 301 | config = definitions."''${ref}".config // { 302 | ''${mergeKey} = mkOverride 1002 (convertName name); 303 | }; 304 | }); 305 | 306 | submoduleForDefinition = ref: resource: kind: group: version: 307 | types.submodule ({name, ...}: { 308 | options = definitions."''${ref}".options // extraOptions; 309 | config = mkMerge ([ 310 | definitions."''${ref}".config 311 | { 312 | kind = mkOptionDefault kind; 313 | apiVersion = mkOptionDefault version; 314 | 315 | # metdata.name cannot use option default, due deep config 316 | metadata.name = mkOptionDefault name; 317 | } 318 | ] ++ (config.defaults.''${resource} or []) 319 | ++ (config.defaults.all or [])); 320 | }); 321 | 322 | coerceAttrsOfSubmodulesToListByKey = ref: mergeKey: (types.coercedTo 323 | (types.listOf (submoduleOf ref)) 324 | (mergeValuesByKey mergeKey) 325 | (types.attrsOf (submoduleWithMergeOf ref mergeKey)) 326 | ); 327 | 328 | definitions = { 329 | ${concatStrings (mapAttrsToList (name: value: '' 330 | "${name}" = {${optionalString (hasAttr "options" value) " 331 | options = {${concatStrings (mapAttrsToList (name: value: '' 332 | "${name}" = ${value}; 333 | '') value.options)}}; 334 | "} 335 | 336 | ${optionalString (hasAttr "config" value) '' 337 | config = {${concatStrings (mapAttrsToList (name: value: '' 338 | "${name}" = ${value}; 339 | '') value.config)}}; 340 | ''} 341 | }; 342 | '') definitions)} 343 | } // (import ./overrides.nix {inheirt definitions lib;})); 344 | in { 345 | kubernetes.customResources = [ 346 | ${concatMapStrings 347 | (resource: ''{ 348 | group = "${resource.group}"; 349 | version = "${resource.version}"; 350 | kind = "${resource.kind}"; 351 | description = ""; 352 | module = definitions."${resource.ref}"; 353 | }'') 354 | (genResources swagger)} 355 | ]; 356 | } 357 | ''; 358 | in 359 | pkgs.runCommand "istio-gen.nix" 360 | { 361 | buildInputs = [ pkgs.nixpkgs-fmt ]; 362 | } '' 363 | cat << 'GENERATED' > ./raw 364 | "${generated}" 365 | GENERATED 366 | 367 | nixpkgs-fmt ./raw 368 | cp ./raw $out 369 | '' 370 | -------------------------------------------------------------------------------- /modules/k8s.nix: -------------------------------------------------------------------------------- 1 | # K8S module defines kubernetes definitions for kubenix 2 | 3 | { options, config, lib, pkgs, k8s, ... }: 4 | 5 | with lib; 6 | let 7 | cfg = config.kubernetes; 8 | 9 | gvkKeyFn = type: "${type.group}/${type.version}/${type.kind}"; 10 | 11 | getDefaults = resource: group: version: kind: 12 | catAttrs "default" (filter 13 | (default: 14 | (resource == null || default.resource == null || default.resource == resource) && 15 | (default.group == null || default.group == group) && 16 | (default.version == null || default.version == version) && 17 | (default.kind == null || default.kind == kind) 18 | ) 19 | cfg.api.defaults); 20 | 21 | moduleToAttrs = value: 22 | if isAttrs value 23 | then mapAttrs (n: v: moduleToAttrs v) (filterAttrs (n: v: v != null && !(hasPrefix "_" n)) value) 24 | 25 | else if isList value 26 | then map (v: moduleToAttrs v) value 27 | 28 | else value; 29 | 30 | apiOptions = { config, ... }: { 31 | options = { 32 | definitions = mkOption { 33 | description = "Attribute set of kubernetes definitions"; 34 | }; 35 | 36 | defaults = mkOption { 37 | description = "Kubernetes defaults to apply to resources"; 38 | type = types.listOf (types.submodule ({ config, ... }: { 39 | options = { 40 | group = mkOption { 41 | description = "Group to apply default to (all by default)"; 42 | type = types.nullOr types.str; 43 | default = null; 44 | }; 45 | 46 | version = mkOption { 47 | description = "Version to apply default to (all by default)"; 48 | type = types.nullOr types.str; 49 | default = null; 50 | }; 51 | 52 | kind = mkOption { 53 | description = "Kind to apply default to (all by default)"; 54 | type = types.nullOr types.str; 55 | default = null; 56 | }; 57 | 58 | resource = mkOption { 59 | description = "Resource to apply default to (all by default)"; 60 | type = types.nullOr types.str; 61 | default = null; 62 | }; 63 | 64 | propagate = mkOption { 65 | description = "Whether to propagate defaults"; 66 | type = types.bool; 67 | default = false; 68 | }; 69 | 70 | default = mkOption { 71 | description = "Default to apply"; 72 | type = types.unspecified; 73 | default = { }; 74 | }; 75 | }; 76 | })); 77 | default = [ ]; 78 | apply = unique; 79 | }; 80 | 81 | types = mkOption { 82 | description = "List of registered kubernetes types"; 83 | type = coerceListOfSubmodulesToAttrs 84 | { 85 | options = { 86 | group = mkOption { 87 | description = "Resource type group"; 88 | type = types.str; 89 | }; 90 | 91 | version = mkOption { 92 | description = "Resoruce type version"; 93 | type = types.str; 94 | }; 95 | 96 | kind = mkOption { 97 | description = "Resource type kind"; 98 | type = types.str; 99 | }; 100 | 101 | name = mkOption { 102 | description = "Resource type name"; 103 | type = types.nullOr types.str; 104 | }; 105 | 106 | attrName = mkOption { 107 | description = "Name of the nixified attribute"; 108 | type = types.str; 109 | }; 110 | }; 111 | } 112 | gvkKeyFn; 113 | default = { }; 114 | }; 115 | }; 116 | 117 | config = { 118 | # apply aliased option 119 | resources = mkAliasDefinitions options.kubernetes.resources; 120 | }; 121 | }; 122 | 123 | indexOf = lst: value: 124 | head (filter (v: v != -1) (imap0 (i: v: if v == value then i else -1) lst)); 125 | 126 | compareVersions = ver1: ver2: 127 | let 128 | getVersion = v: substring 1 10 v; 129 | splittedVer1 = builtins.splitVersion (getVersion ver1); 130 | splittedVer2 = builtins.splitVersion (getVersion ver2); 131 | 132 | v1 = if length splittedVer1 == 1 then "${getVersion ver1}prod" else getVersion ver1; 133 | v2 = if length splittedVer2 == 1 then "${getVersion ver2}prod" else getVersion ver2; 134 | in 135 | builtins.compareVersions v1 v2; 136 | 137 | customResourceTypesByAttrName = zipAttrs (mapAttrsToList 138 | (_: resourceType: { 139 | ${resourceType.attrName} = resourceType; 140 | }) 141 | cfg.customTypes); 142 | 143 | customResourceTypesByAttrNameSortByVersion = mapAttrs 144 | (_: resourceTypes: 145 | reverseList (sort 146 | (r1: r2: 147 | compareVersions r1.version r2.version > 0 148 | ) 149 | resourceTypes) 150 | ) 151 | customResourceTypesByAttrName; 152 | 153 | latestCustomResourceTypes = 154 | mapAttrsToList (_: resources: last resources) customResourceTypesByAttrNameSortByVersion; 155 | 156 | customResourceModuleForType = config: ct: { name, ... }: { 157 | imports = getDefaults ct.name ct.group ct.version ct.kind; 158 | options = { 159 | apiVersion = mkOption { 160 | description = "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources"; 161 | type = types.nullOr types.str; 162 | }; 163 | 164 | kind = mkOption { 165 | description = "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds"; 166 | type = types.nullOr types.str; 167 | }; 168 | 169 | metadata = mkOption { 170 | description = "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata."; 171 | type = types.nullOr (types.submodule config.definitions."io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"); 172 | }; 173 | 174 | spec = mkOption { 175 | description = "Module spec"; 176 | type = types.either types.attrs (types.submodule ct.module); 177 | default = { }; 178 | }; 179 | }; 180 | 181 | config = { 182 | apiVersion = mkOptionDefault "${ct.group}/${ct.version}"; 183 | kind = mkOptionDefault ct.kind; 184 | metadata.name = mkDefault name; 185 | }; 186 | }; 187 | 188 | customResourceOptions = (mapAttrsToList 189 | (_: ct: { config, ... }: 190 | let 191 | module = customResourceModuleForType config ct; 192 | in 193 | { 194 | options.resources.${ct.group}.${ct.version}.${ct.kind} = mkOption { 195 | description = ct.description; 196 | type = types.attrsOf (types.submodule module); 197 | default = { }; 198 | }; 199 | }) 200 | cfg.customTypes) ++ (map 201 | (ct: { options, config, ... }: 202 | let 203 | module = customResourceModuleForType config ct; 204 | in 205 | { 206 | options.resources.${ct.attrName} = mkOption { 207 | description = ct.description; 208 | type = types.attrsOf (types.submodule module); 209 | default = { }; 210 | }; 211 | 212 | config.resources.${ct.group}.${ct.version}.${ct.kind} = 213 | mkAliasDefinitions options.resources.${ct.attrName}; 214 | }) 215 | latestCustomResourceTypes); 216 | 217 | coerceListOfSubmodulesToAttrs = submodule: keyFn: 218 | let 219 | mergeValuesByFn = keyFn: values: 220 | listToAttrs (map 221 | (value: 222 | nameValuePair (toString (keyFn value)) value 223 | ) 224 | values); 225 | 226 | # Either value of type `finalType` or `coercedType`, the latter is 227 | # converted to `finalType` using `coerceFunc`. 228 | coercedTo = coercedType: coerceFunc: finalType: 229 | mkOptionType rec { 230 | name = "coercedTo"; 231 | description = "${finalType.description} or ${coercedType.description}"; 232 | check = x: finalType.check x || coercedType.check x; 233 | merge = loc: defs: 234 | let 235 | coerceVal = val: 236 | if finalType.check val then 237 | val 238 | else 239 | let coerced = coerceFunc val; in assert finalType.check coerced; coerced; 240 | 241 | in 242 | finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 243 | getSubOptions = finalType.getSubOptions; 244 | getSubModules = finalType.getSubModules; 245 | substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 246 | typeMerge = t1: t2: null; 247 | functor = (defaultFunctor name) // { wrapped = finalType; }; 248 | }; 249 | in 250 | coercedTo 251 | (types.listOf (types.submodule submodule)) 252 | (mergeValuesByFn keyFn) 253 | (types.attrsOf (types.submodule submodule)); 254 | 255 | in 256 | { 257 | imports = [ ./base.nix ]; 258 | 259 | options.kubernetes = { 260 | version = mkOption { 261 | description = "Kubernetes version to use"; 262 | type = types.enum [ "1.19" "1.20" "1.21" ]; 263 | default = "1.21"; 264 | }; 265 | 266 | namespace = mkOption { 267 | description = "Default namespace where to deploy kubernetes resources"; 268 | type = types.nullOr types.str; 269 | default = null; 270 | }; 271 | 272 | resourceOrder = mkOption { 273 | description = "Preffered resource order"; 274 | type = types.listOf types.str; 275 | default = [ 276 | "CustomResourceDefinition" 277 | "Namespace" 278 | ]; 279 | }; 280 | 281 | api = mkOption { 282 | type = types.submodule { 283 | imports = [ 284 | (./generated + ''/v'' + cfg.version + ".nix") 285 | apiOptions 286 | ] ++ customResourceOptions; 287 | }; 288 | default = { }; 289 | }; 290 | 291 | imports = mkOption { 292 | type = types.listOf (types.either types.package types.path); 293 | description = "List of resources to import"; 294 | default = [ ]; 295 | }; 296 | 297 | resources = mkOption { 298 | description = "Alias for `config.kubernetes.api.resources` options"; 299 | default = { }; 300 | type = types.attrsOf types.attrs; 301 | }; 302 | 303 | customTypes = mkOption { 304 | description = "List of custom resource types to make API for"; 305 | type = coerceListOfSubmodulesToAttrs 306 | { 307 | options = { 308 | group = mkOption { 309 | description = "Custom type group"; 310 | type = types.str; 311 | }; 312 | 313 | version = mkOption { 314 | description = "Custom type version"; 315 | type = types.str; 316 | }; 317 | 318 | kind = mkOption { 319 | description = "Custom type kind"; 320 | type = types.str; 321 | }; 322 | 323 | name = mkOption { 324 | description = "Custom type resource name"; 325 | type = types.nullOr types.str; 326 | default = null; 327 | }; 328 | 329 | attrName = mkOption { 330 | description = "Name of the nixified attribute"; 331 | type = types.str; 332 | }; 333 | 334 | description = mkOption { 335 | description = "Custom type description"; 336 | type = types.str; 337 | default = ""; 338 | }; 339 | 340 | module = mkOption { 341 | description = "Custom type module"; 342 | type = types.unspecified; 343 | default = { }; 344 | }; 345 | }; 346 | } 347 | gvkKeyFn; 348 | default = { }; 349 | }; 350 | 351 | objects = mkOption { 352 | description = "List of generated kubernetes objects"; 353 | type = types.listOf types.attrs; 354 | apply = items: sort 355 | (r1: r2: 356 | if elem r1.kind cfg.resourceOrder && elem r2.kind cfg.resourceOrder 357 | then indexOf cfg.resourceOrder r1.kind < indexOf cfg.resourceOrder r2.kind 358 | else if elem r1.kind cfg.resourceOrder then true else false 359 | ) 360 | (unique items); 361 | default = [ ]; 362 | }; 363 | 364 | generated = mkOption { 365 | description = "Generated kubernetes list object"; 366 | type = types.attrs; 367 | }; 368 | 369 | result = mkOption { 370 | description = "Generated kubernetes JSON file"; 371 | type = types.package; 372 | }; 373 | 374 | resultYAML = mkOption { 375 | description = "Genrated kubernetes YAML file"; 376 | type = types.package; 377 | }; 378 | }; 379 | 380 | config = { 381 | # features that module is defining 382 | _m.features = [ "k8s" ]; 383 | 384 | # module propagation options 385 | _m.propagate = [{ 386 | features = [ "k8s" ]; 387 | module = { config, ... }: { 388 | # propagate kubernetes version and namespace 389 | kubernetes.version = mkDefault cfg.version; 390 | kubernetes.namespace = mkDefault cfg.namespace; 391 | }; 392 | } 393 | { 394 | features = [ "k8s" "submodule" ]; 395 | module = { config, ... }: { 396 | # set module defaults 397 | kubernetes.api.defaults = ( 398 | # propagate defaults if default propagation is enabled 399 | (filter (default: default.propagate) cfg.api.defaults) ++ 400 | 401 | [ 402 | # set module name and version for all kuberentes resources 403 | { 404 | default.metadata.labels = { 405 | "kubenix/module-name" = config.submodule.name; 406 | "kubenix/module-version" = config.submodule.version; 407 | }; 408 | } 409 | ] 410 | ); 411 | }; 412 | }]; 413 | 414 | # expose k8s helper methods as module argument 415 | _module.args.k8s = import ../lib/k8s { inherit lib; }; 416 | 417 | kubernetes.api = mkMerge ([{ 418 | # register custom types 419 | types = mapAttrsToList 420 | (_: cr: { 421 | inherit (cr) name group version kind attrName; 422 | }) 423 | cfg.customTypes; 424 | 425 | defaults = [{ 426 | default = { 427 | # set default kubernetes namespace to all resources 428 | metadata.namespace = mkIf (config.kubernetes.namespace != null) 429 | (mkDefault config.kubernetes.namespace); 430 | 431 | # set project name to all resources 432 | metadata.annotations = { 433 | "kubenix/project-name" = config.kubenix.project; 434 | "kubenix/k8s-version" = cfg.version; 435 | }; 436 | }; 437 | }]; 438 | }] ++ 439 | 440 | # import of yaml files 441 | (map 442 | (i: 443 | let 444 | # load yaml file 445 | object = importYAML i; 446 | groupVersion = splitString "/" object.apiVersion; 447 | name = object.metadata.name; 448 | version = last groupVersion; 449 | group = 450 | if version == (head groupVersion) 451 | then "core" else head groupVersion; 452 | kind = object.kind; 453 | in 454 | { 455 | resources.${group}.${version}.${kind}.${name} = object; 456 | }) 457 | cfg.imports)); 458 | 459 | kubernetes.objects = flatten (mapAttrsToList 460 | (_: type: 461 | mapAttrsToList (name: resource: moduleToAttrs resource) 462 | cfg.api.resources.${type.group}.${type.version}.${type.kind} 463 | ) 464 | cfg.api.types); 465 | 466 | kubernetes.generated = k8s.mkHashedList { 467 | items = config.kubernetes.objects; 468 | labels."kubenix/project-name" = config.kubenix.project; 469 | labels."kubenix/k8s-version" = config.kubernetes.version; 470 | }; 471 | 472 | kubernetes.result = 473 | pkgs.writeText "${config.kubenix.project}-generated.json" (builtins.toJSON cfg.generated); 474 | 475 | kubernetes.resultYAML = 476 | toMultiDocumentYaml "${config.kubenix.project}-generated.yaml" (config.kubernetes.objects); 477 | }; 478 | } 479 | -------------------------------------------------------------------------------- /jobs/generators/k8s/default.nix: -------------------------------------------------------------------------------- 1 | { name 2 | , pkgs 3 | , lib 4 | , spec 5 | }: 6 | 7 | with lib; 8 | let 9 | gen = rec { 10 | mkMerge = values: ''mkMerge [${concatMapStrings 11 | (value: " 12 | ${value} 13 | ") 14 | values}]''; 15 | 16 | toNixString = value: 17 | if isAttrs value || isList value 18 | then builtins.toJSON value 19 | else if isString value 20 | then ''"${value}"'' 21 | else if value == null 22 | then "null" 23 | else builtins.toString value; 24 | 25 | removeEmptyLines = str: concatStringsSep "\n" (filter (l: (builtins.match "( |)+" l) == null) (splitString "\n" str)); 26 | 27 | mkOption = 28 | { description ? null 29 | , type ? null 30 | , default ? null 31 | , apply ? null 32 | }: removeEmptyLines ''mkOption { 33 | ${optionalString (description != null) "description = ${builtins.toJSON description};"} 34 | ${optionalString (type != null) ''type = ${type};''} 35 | ${optionalString (default != null) ''default = ${toNixString default};''} 36 | ${optionalString (apply != null) ''apply = ${apply};''} 37 | }''; 38 | 39 | mkOverride = priority: value: "mkOverride ${toString priority} ${toNixString value}"; 40 | 41 | types = { 42 | unspecified = "types.unspecified"; 43 | str = "types.str"; 44 | int = "types.int"; 45 | bool = "types.bool"; 46 | attrs = "types.attrs"; 47 | nullOr = val: "(types.nullOr ${val})"; 48 | attrsOf = val: "(types.attrsOf ${val})"; 49 | listOf = val: "(types.listOf ${val})"; 50 | coercedTo = coercedType: coerceFunc: finalType: 51 | "(types.coercedTo ${coercedType} ${coerceFunc} ${finalType})"; 52 | either = val1: val2: "(types.either ${val1} ${val2})"; 53 | loaOf = type: "(types.loaOf ${type})"; 54 | }; 55 | 56 | hasTypeMapping = def: 57 | hasAttr "type" def && 58 | elem def.type [ "string" "integer" "boolean" ]; 59 | 60 | mergeValuesByKey = mergeKey: ''(mergeValuesByKey "${mergeKey}")''; 61 | 62 | mapType = def: 63 | if def.type == "string" then 64 | if hasAttr "format" def && def.format == "int-or-string" 65 | then types.either types.int types.str 66 | else types.str 67 | else if def.type == "integer" then types.int 68 | else if def.type == "number" then types.int 69 | else if def.type == "boolean" then types.bool 70 | else if def.type == "object" then types.attrs 71 | else throw "type ${def.type} not supported"; 72 | 73 | submoduleOf = definitions: ref: ''(submoduleOf "${ref}")''; 74 | 75 | submoduleForDefinition = ref: name: kind: group: version: 76 | ''(submoduleForDefinition "${ref}" "${name}" "${kind}" "${group}" "${version}")''; 77 | 78 | coerceAttrsOfSubmodulesToListByKey = ref: mergeKey: 79 | ''(coerceAttrsOfSubmodulesToListByKey "${ref}" "${mergeKey}")''; 80 | 81 | attrsToList = "values: if values != null then mapAttrsToList (n: v: v) values else values"; 82 | 83 | refDefinition = attr: head (tail (tail (splitString "/" attr."$ref"))); 84 | }; 85 | 86 | refType = attr: head (tail (tail (splitString "/" attr."$ref"))); 87 | 88 | compareVersions = ver1: ver2: 89 | let 90 | getVersion = v: substring 1 10 v; 91 | splitVersion = v: builtins.splitVersion (getVersion v); 92 | isAlpha = v: elem "alpha" (splitVersion v); 93 | patchVersion = v: 94 | if isAlpha v then "" 95 | else if length (splitVersion v) == 1 then "${getVersion v}prod" 96 | else getVersion v; 97 | 98 | v1 = patchVersion ver1; 99 | v2 = patchVersion ver2; 100 | in 101 | builtins.compareVersions v1 v2; 102 | 103 | fixJSON = content: replaceStrings [ "\\u" ] [ "u" ] content; 104 | 105 | fetchSpecs = path: builtins.fromJSON (fixJSON (builtins.readFile path)); 106 | 107 | genDefinitions = swagger: with gen; mapAttrs 108 | (name: definition: 109 | # if $ref is in definition it means it's an alias of other definition 110 | if hasAttr "$ref" definition 111 | then definitions."${refDefinition definition}" 112 | 113 | else if !(hasAttr "properties" definition) 114 | then { } 115 | 116 | # in other case it's an actual definition 117 | else { 118 | options = mapAttrs 119 | (propName: property: 120 | let 121 | isRequired = elem propName (definition.required or [ ]); 122 | requiredOrNot = type: if isRequired then type else types.nullOr type; 123 | optionProperties = 124 | 125 | # if $ref is in property it references other definition, 126 | # but if other definition does not have properties, then just take it's type 127 | if hasAttr "$ref" property then 128 | if hasTypeMapping swagger.definitions.${refDefinition property} then { 129 | type = requiredOrNot (mapType swagger.definitions.${refDefinition property}); 130 | } 131 | else { 132 | type = requiredOrNot (submoduleOf definitions (refDefinition property)); 133 | } 134 | 135 | # if property has an array type 136 | else if property.type == "array" then 137 | 138 | # if reference is in items it can reference other type of another 139 | # definition 140 | if hasAttr "$ref" property.items then 141 | 142 | # if it is a reference to simple type 143 | if hasTypeMapping swagger.definitions.${refDefinition property.items} 144 | then { 145 | type = requiredOrNot (types.listOf (mapType swagger.definitions.${refDefinition property.items}.type)); 146 | } 147 | 148 | # if a reference is to complex type 149 | else 150 | # if x-kubernetes-patch-merge-key is set then make it an 151 | # attribute set of submodules 152 | if hasAttr "x-kubernetes-patch-merge-key" property 153 | then 154 | let 155 | mergeKey = property."x-kubernetes-patch-merge-key"; 156 | in 157 | { 158 | type = requiredOrNot (coerceAttrsOfSubmodulesToListByKey (refDefinition property.items) mergeKey); 159 | apply = attrsToList; 160 | } 161 | 162 | # in other case it's a simple list 163 | else { 164 | type = requiredOrNot (types.listOf (submoduleOf definitions (refDefinition property.items))); 165 | } 166 | 167 | # in other case it only references a simple type 168 | else { 169 | type = requiredOrNot (types.listOf (mapType property.items)); 170 | } 171 | 172 | else if property.type == "object" && hasAttr "additionalProperties" property 173 | then 174 | # if it is a reference to simple type 175 | if ( 176 | hasAttr "$ref" property.additionalProperties && 177 | hasTypeMapping swagger.definitions.${refDefinition property.additionalProperties} 178 | ) then { 179 | type = requiredOrNot (types.attrsOf (mapType swagger.definitions.${refDefinition property.additionalProperties})); 180 | } 181 | 182 | else if hasAttr "$ref" property.additionalProperties 183 | then { 184 | type = requiredOrNot types.attrs; 185 | } 186 | 187 | # if is an array 188 | else if property.additionalProperties.type == "array" 189 | then { 190 | type = requiredOrNot (types.loaOf (mapType property.additionalProperties.items)); 191 | } 192 | 193 | else { 194 | type = requiredOrNot (types.attrsOf (mapType property.additionalProperties)); 195 | } 196 | 197 | # just a simple property 198 | else { 199 | type = requiredOrNot (mapType property); 200 | }; 201 | in 202 | mkOption ({ 203 | description = property.description or ""; 204 | } // optionProperties) 205 | ) 206 | definition.properties; 207 | config = 208 | let 209 | optionalProps = filterAttrs 210 | (propName: property: 211 | !(elem propName (definition.required or [ ])) 212 | ) 213 | definition.properties; 214 | in 215 | mapAttrs (name: property: mkOverride 1002 null) optionalProps; 216 | } 217 | ) 218 | swagger.definitions; 219 | 220 | mapCharPairs = f: s1: s2: concatStrings (imap0 221 | (i: c1: 222 | f i c1 (if i >= stringLength s2 then "" else elemAt (stringToCharacters s2) i) 223 | ) 224 | (stringToCharacters s1)); 225 | 226 | getAttrName = resource: kind: 227 | mapCharPairs 228 | (i: c1: c2: 229 | if hasPrefix "API" kind && i == 0 then "A" 230 | else if i == 0 then c1 231 | else if c2 == "" || (toLower c2) != c1 then c1 232 | else c2 233 | ) 234 | resource 235 | kind; 236 | 237 | genResourceTypes = swagger: mapAttrs' 238 | (name: path: 239 | let 240 | ref = refType (head path.post.parameters).schema; 241 | group' = path.post."x-kubernetes-group-version-kind".group; 242 | version' = path.post."x-kubernetes-group-version-kind".version; 243 | kind' = path.post."x-kubernetes-group-version-kind".kind; 244 | name' = last (splitString "/" name); 245 | attrName = getAttrName name' kind'; 246 | in 247 | nameValuePair ref { 248 | inherit ref attrName; 249 | 250 | name = name'; 251 | group = if group' == "" then "core" else group'; 252 | version = version'; 253 | kind = kind'; 254 | description = swagger.definitions.${ref}.description; 255 | defintion = refDefinition (head path.post.parameters).schema; 256 | }) 257 | (filterAttrs 258 | (name: path: 259 | hasAttr "post" path && 260 | path.post."x-kubernetes-action" == "post" 261 | ) 262 | swagger.paths); 263 | 264 | swagger = fetchSpecs spec; 265 | definitions = genDefinitions swagger; 266 | resourceTypes = genResourceTypes swagger; 267 | 268 | resourceTypesByKind = zipAttrs (mapAttrsToList 269 | (name: resourceType: { 270 | ${resourceType.kind} = resourceType; 271 | }) 272 | resourceTypes); 273 | 274 | resourcesTypesByKindSortByVersion = mapAttrs 275 | (kind: resourceTypes: 276 | reverseList (sort 277 | (r1: r2: 278 | compareVersions r1.version r2.version > 0 279 | ) 280 | resourceTypes) 281 | ) 282 | resourceTypesByKind; 283 | 284 | latestResourceTypesByKind = 285 | mapAttrs (kind: resources: last resources) resourcesTypesByKindSortByVersion; 286 | 287 | genResourceOptions = resource: with gen; let 288 | submoduleForDefinition' = definition: 289 | let 290 | in 291 | submoduleForDefinition 292 | definition.ref 293 | definition.name 294 | definition.kind 295 | definition.group 296 | definition.version; 297 | in 298 | mkOption { 299 | description = resource.description; 300 | type = types.attrsOf (submoduleForDefinition' resource); 301 | default = { }; 302 | }; 303 | 304 | generated = '' 305 | # This file was generated with kubenix k8s generator, do not edit 306 | { lib, options, config, ... }: 307 | 308 | with lib; 309 | 310 | let 311 | getDefaults = resource: group: version: kind: 312 | catAttrs "default" (filter (default: 313 | (default.resource == null || default.resource == resource) && 314 | (default.group == null || default.group == group) && 315 | (default.version == null || default.version == version) && 316 | (default.kind == null || default.kind == kind) 317 | ) config.defaults); 318 | 319 | types = lib.types // rec { 320 | str = mkOptionType { 321 | name = "str"; 322 | description = "string"; 323 | check = isString; 324 | merge = mergeEqualOption; 325 | }; 326 | 327 | # Either value of type `finalType` or `coercedType`, the latter is 328 | # converted to `finalType` using `coerceFunc`. 329 | coercedTo = coercedType: coerceFunc: finalType: 330 | mkOptionType rec { 331 | name = "coercedTo"; 332 | description = "''${finalType.description} or ''${coercedType.description}"; 333 | check = x: finalType.check x || coercedType.check x; 334 | merge = loc: defs: 335 | let 336 | coerceVal = val: 337 | if finalType.check val then val 338 | else let 339 | coerced = coerceFunc val; 340 | in assert finalType.check coerced; coerced; 341 | 342 | in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs); 343 | getSubOptions = finalType.getSubOptions; 344 | getSubModules = finalType.getSubModules; 345 | substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m); 346 | typeMerge = t1: t2: null; 347 | functor = (defaultFunctor name) // { wrapped = finalType; }; 348 | }; 349 | }; 350 | 351 | mkOptionDefault = mkOverride 1001; 352 | 353 | mergeValuesByKey = mergeKey: values: 354 | listToAttrs (map 355 | (value: nameValuePair ( 356 | if isAttrs value.''${mergeKey} 357 | then toString value.''${mergeKey}.content 358 | else (toString value.''${mergeKey}) 359 | ) value) 360 | values); 361 | 362 | submoduleOf = ref: types.submodule ({name, ...}: { 363 | options = definitions."''${ref}".options or {}; 364 | config = definitions."''${ref}".config or {}; 365 | }); 366 | 367 | submoduleWithMergeOf = ref: mergeKey: types.submodule ({name, ...}: let 368 | convertName = name: 369 | if definitions."''${ref}".options.''${mergeKey}.type == types.int 370 | then toInt name 371 | else name; 372 | in { 373 | options = definitions."''${ref}".options; 374 | config = definitions."''${ref}".config // { 375 | ''${mergeKey} = mkOverride 1002 (convertName name); 376 | }; 377 | }); 378 | 379 | submoduleForDefinition = ref: resource: kind: group: version: let 380 | apiVersion = if group == "core" then version else "''${group}/''${version}"; 381 | in types.submodule ({name, ...}: { 382 | imports = getDefaults resource group version kind; 383 | options = definitions."''${ref}".options; 384 | config = mkMerge [ 385 | definitions."''${ref}".config 386 | { 387 | kind = mkOptionDefault kind; 388 | apiVersion = mkOptionDefault apiVersion; 389 | 390 | # metdata.name cannot use option default, due deep config 391 | metadata.name = mkOptionDefault name; 392 | } 393 | ]; 394 | }); 395 | 396 | coerceAttrsOfSubmodulesToListByKey = ref: mergeKey: (types.coercedTo 397 | (types.listOf (submoduleOf ref)) 398 | (mergeValuesByKey mergeKey) 399 | (types.attrsOf (submoduleWithMergeOf ref mergeKey)) 400 | ); 401 | 402 | definitions = { 403 | ${concatStrings (mapAttrsToList (name: value: '' 404 | "${name}" = { 405 | ${optionalString (hasAttr "options" value) " 406 | options = {${concatStrings (mapAttrsToList (name: value: '' 407 | "${name}" = ${value}; 408 | '') value.options)}}; 409 | "} 410 | 411 | ${optionalString (hasAttr "config" value) '' 412 | config = {${concatStrings (mapAttrsToList (name: value: '' 413 | "${name}" = ${value}; 414 | '') value.config)}}; 415 | ''} 416 | }; 417 | '') definitions)} 418 | }; 419 | in { 420 | # all resource versions 421 | options = { 422 | resources = { 423 | ${concatStrings (mapAttrsToList (_: rt: '' 424 | "${rt.group}"."${rt.version}"."${rt.kind}" = ${genResourceOptions rt}; 425 | '') resourceTypes)} 426 | } // { 427 | ${concatStrings (mapAttrsToList (_: rt: '' 428 | "${rt.attrName}" = ${genResourceOptions rt}; 429 | '') latestResourceTypesByKind)} 430 | }; 431 | }; 432 | 433 | config = { 434 | # expose resource definitions 435 | inherit definitions; 436 | 437 | # register resource types 438 | types = [${concatStrings (mapAttrsToList (_: rt: ''{ 439 | name = "${rt.name}"; 440 | group = "${rt.group}"; 441 | version = "${rt.version}"; 442 | kind = "${rt.kind}"; 443 | attrName = "${rt.attrName}"; 444 | }'') resourceTypes)}]; 445 | 446 | resources = { 447 | ${concatStrings (mapAttrsToList (_: rt: '' 448 | "${rt.group}"."${rt.version}"."${rt.kind}" = 449 | mkAliasDefinitions options.resources."${rt.attrName}"; 450 | '') latestResourceTypesByKind)} 451 | }; 452 | }; 453 | } 454 | ''; 455 | in 456 | pkgs.runCommand "k8s-${name}-gen.nix" 457 | { 458 | buildInputs = [ pkgs.nixpkgs-fmt ]; 459 | } '' 460 | cat << 'GENERATED' > ./raw 461 | "${generated}" 462 | GENERATED 463 | 464 | nixpkgs-fmt ./raw 465 | cp ./raw $out 466 | '' 467 | --------------------------------------------------------------------------------