Skip to content

Instantly share code, notes, and snippets.

@johanot
Created October 30, 2018 16:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johanot/2de1390a04e7c357bde6dc0abcaec5b8 to your computer and use it in GitHub Desktop.
Save johanot/2de1390a04e7c357bde6dc0abcaec5b8 to your computer and use it in GitHub Desktop.
{ 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