Created
October 30, 2018 16:46
-
-
Save johanot/2de1390a04e7c357bde6dc0abcaec5b8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ config, pkgs, lib, ... }: | |
with lib; | |
let | |
desc = "Kubernetes DBC Addons"; | |
cfg = config.services.kubernetes.dbc.addons; | |
files = with builtins; dir: | |
remove null | |
(pkgs.lib.mapAttrsToList | |
(name: _type: | |
let | |
# We only want addon manifests that match below pattern | |
filePattern = "^(((.+)\.yaml)|((.+)\.yml)|((.+)\.json))$"; | |
fileMatch = match filePattern name; | |
cleanName = with builtins; file: | |
removePrefix "-" | |
(replaceStrings [(toString cfg.manifestPath) "/" "."] ["" "-" "-"] file); | |
fileName = (if isList fileMatch then cleanName (last (remove null fileMatch)) else null); | |
fqn = d: f: toString (d + "/${f}"); # toString syntax is needed to make nix NOT coerce fqn as path | |
in | |
(if _type == "directory" then files (fqn dir name) # If type is directory, recurse into subdir | |
else if isNull fileName then null # Return null here to avoid adding an unwanted item to store | |
else { # Assuming this is a file we want to create an addon for.. | |
name = cleanName (fqn dir fileName); | |
path = (path { path = (fqn dir name); }); # builtins.path copies manifestFile to /nix/store | |
})) | |
(readDir dir)); | |
# Adding a dummy object configuration for all manifest derivations that are created from the manifestPath | |
readManifests = (map (item: | |
#builtins.trace item.path | |
(unit "manifestFile" item.name { | |
manifestFile = item.path; | |
lib = {}; | |
apiVersion = "*"; | |
kind = "manifest"; | |
allNamespaces = false; | |
namespace = "*"; | |
reconcileDelay = 20; | |
reconcileRandomDeviation = 10; | |
})) | |
(flatten (files cfg.manifestPath))); | |
# Leader determination script forked from Kubernetes addon manager | |
amILeader = with pkgs; writeScript "amIleader" '' | |
#! ${bash}/bin/bash -e | |
KUBE_CONTROLLER_MANAGER_LEADER=$(${kubectl}/bin/kubectl -n kube-system get ep kube-controller-manager \ | |
-o go-template=$'{{index .metadata.annotations "control-plane.alpha.kubernetes.io/leader"}}' \ | |
| ${gnused}/bin/sed 's/^.*"holderIdentity":"\([^"]*\)".*/\1/' | ${gawk}/bin/awk -F'_' '{print $1}') | |
exit $([[ $KUBE_CONTROLLER_MANAGER_LEADER == $HOSTNAME ]]) | |
''; | |
unit = with pkgs; type: name: object: ( | |
let | |
# The object is namespaceable if it's not a clusterwide object (by definition) | |
isNamespaceable = (!(any (k: k == header.kind)) clusterwides); | |
# The object spec is produced either literally from the object.spec attribute, or | |
# by invoking the mkSpec function on the object, if that is defined by the object submodule | |
spec = if builtins.hasAttr "mkSpec" object.lib && builtins.isAttrs object.spec | |
then throw "You cannot set a custom spec on an object that already has a spec generator (${fqn})" | |
else (if builtins.hasAttr "mkSpec" object.lib then object.lib.mkSpec name object else { spec = object.spec; }); | |
# The object namespace is set either to the literal value defined on the object itself, or | |
# to nothing (empty string) in either one of two cases: | |
# 1) The object is clusterwide (non-namespaceable) | |
# 2) The object is set to be applied in allNamespaces | |
ns = if isNamespaceable then | |
if object.allNamespaces then | |
(if object.namespace == "" then "*" | |
else throw "Applying to 'allNamespaces' while explicitly providing a namespace (${fqn})") | |
else | |
(if object.namespace != "" then object.namespace | |
else throw "You have to either provide a namespace or set 'allNamespace' = true (${fqn})") | |
else | |
(if object.namespace == "" && object.allNamespaces == false then null | |
else throw "Object is clusterwide, so setting 'allNamespaces' or 'namespace' makes no sense (${fqn})"); | |
# The object header is produced either literally from the apiVersion and kind-attributes, or | |
# by invoking the mkHeader function on the object, if that is defined by the object submodule | |
header = if builtins.hasAttr "mkHeader" object.lib | |
then object.lib.mkHeader object | |
else { apiVersion = object.apiVersion; kind = object.kind; }; | |
# The final manifest for the addon object is build below in JSON format | |
# every object contains apiVersion and kind as well as some metadata and possibly a namespace, if the object is not clusterwide | |
# objects also have an optional 'spec' in a format which is specific to each object type | |
manifest = | |
if builtins.hasAttr "manifestFile" object then object.manifestFile | |
else | |
pkgs.writeText "${fqn}.json" (builtins.toJSON ({ | |
apiVersion = header.apiVersion; | |
kind = header.kind; | |
metadata = { | |
labels = object.labels; | |
name = if object.name != "" then object.name else toLower name; | |
} // | |
{ namespace = ns; }; | |
} // | |
optionalAttrs (builtins.isAttrs spec) spec | |
)); | |
preProcess = "function preProcess { " + | |
(if builtins.hasAttr "preProcess" object.lib then (object.lib.preProcess pkgs object manifest) else "cat ${manifest}; ") + | |
"}"; | |
replaceNS = ''sed 's/"namespace":"\*"/"namespace":"'$NS'"/' ''; | |
# Hack! Some YAML-deployments uses the fully qualified cluster cluster, | |
# and.. lacking a proper templating engine | |
replaceClusterDomain = ''sed 's/''${CLUSTERDOMAIN}/${config.services.kubernetes.addons.dns.clusterDomain}/g' ''; | |
perform = op: if object.allNamespaces then '' | |
NSS=$(kubectl get namespaces -o json | jq -r '.items[] | .metadata.name') | |
${preProcess} | |
for NS in $NSS; do | |
echo "$(preProcess)" | ${replaceNS}| ${replaceClusterDomain}| kubectl ${op} -f - | |
done | |
'' else '' | |
${preProcess} | |
echo "$(preProcess)" | ${replaceClusterDomain}| kubectl ${op} -f - | |
''; | |
script = with pkgs; '' | |
while [[ true ]]; do | |
if ${amILeader}; then | |
${perform "apply"} | |
fi | |
${if object.reconcileRandomDeviation > 0 then | |
"DELAY=$((${toString object.reconcileDelay}+($RANDOM%${toString object.reconcileRandomDeviation})))" | |
else | |
"DELAY=${toString object.reconcileDelay}" | |
} | |
sleep $DELAY | |
done | |
''; | |
postStop = with pkgs; if header.kind == "Namespace" then '' | |
>&2 echo "Not deleting namespace '${name}'. Please delete manually, if needed you really want to." | |
'' | |
else | |
'' | |
if ${amILeader}; then | |
${perform "delete"} | |
fi | |
''; | |
fqn = "${optionalString (isNamespaceable && ns != "*") (object.namespace + "-")}${type}-${name}"; | |
serviceName = "kube-dbc-addons-${fqn}"; | |
in | |
{ | |
services."${serviceName}" = { | |
path = [ jq gnused kubernetes coreutils ]; | |
description = "${desc} - ${fqn}"; | |
wantedBy = ["multi-user.target"]; | |
after = ["kubernetes.target"]; | |
requires = ["kubernetes.target"]; | |
inherit script; | |
inherit postStop; | |
serviceConfig = { | |
Type = "simple"; | |
Restart = "always"; | |
RestartSec = toString object.reconcileDelay; | |
}; | |
}; | |
}); | |
in | |
{ | |
options.services.kubernetes.dbc.addons = with types; { | |
enable = mkEnableOption desc; | |
networkPolicies = mkOption { | |
type = attrsOf (submodule (import ./netpol.nix)); | |
default = {}; | |
}; | |
namespaces = mkOption { | |
# TODO Come up with a not too ugly way of allowing listOf str as well | |
type = attrsOf (submodule (import ./namespace.nix)); | |
default = {}; | |
}; | |
admissions = mkOption { | |
type = attrsOf (submodule (import ./admission-webhook.nix)); | |
default = {}; | |
}; | |
manifests = mkOption { | |
type = attrsOf (submodule (import ./manifest.nix { kind = ""; apiVersion = ""; })); | |
default = {}; | |
}; | |
manifestPath = mkOption { | |
type = nullOr path; | |
default = null; | |
}; | |
}; | |
config = mkIf cfg.enable { | |
systemd = mkMerge ((mapAttrsToList (unit "netpol") cfg.networkPolicies) ++ | |
(mapAttrsToList (unit "namespace") cfg.namespaces) ++ | |
(mapAttrsToList (unit "admission") cfg.admissions) ++ | |
(mapAttrsToList (unit "manifest") cfg.manifests) ++ | |
(optionals (cfg.manifestPath != null) readManifests) | |
); | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment