first commit

This commit is contained in:
Jaka Hudoklin 2017-11-11 11:52:17 +01:00
commit cbf84e25a5
22 changed files with 131008 additions and 0 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
result

6
.travis.yml Normal file
View file

@ -0,0 +1,6 @@
language: node_js
node_js:
- v5
- v4
- '0.12'
- '0.10'

20
README.md Normal file
View file

@ -0,0 +1,20 @@
# KubeNix
> Kubernetes resource builder written in nix
## About
KubeNix is a kubernetes resource builder, that uses nix module system for
definition of kubernetes resources and nix build system for building complex
kubernetes resources very easyly.
### Features
- Loading and override of kubernetes json and yaml files
- Support for complex merging of kubernetes resource definitions
- No more helm stupid yaml templating, nix is a way better templating language
- Support for all kubernetes versions
## License
MIT © [Jaka Hudoklin](https://x-truder.net)

89
default.nix Normal file
View file

@ -0,0 +1,89 @@
{
pkgs ? import <nixpkgs> {}
}:
with pkgs.lib;
with import ./lib.nix { inherit pkgs; inherit (pkgs) lib; };
let
evalKubernetesModules = configuration: evalModules {
modules = [./kubernetes.nix ./modules.nix configuration];
args = {
inherit pkgs;
name = "default";
k8s = { inherit loadJSON loadYAML toBase64; };
};
};
flattenResources = resources: flatten (
mapAttrsToList (name: resourceGroup:
mapAttrsToList (name: resource: resource) resourceGroup
) resources
);
filterResources = resourceFilter: resources:
mapAttrs (groupName: resources:
(filterAttrs (name: resource:
resourceFilter groupName name resource
) resources)
) resources;
toKubernetesList = resources: {
kind = "List";
apiVersion = "v1";
items = resources;
};
removeNixOptions = resources:
map (filterAttrs (name: attr: name != "nix")) resources;
buildResources = {
configuration ? {},
resourceFilter ? groupName: name: resource: true,
withDependencies ? true
}: let
evaldConfiguration = evalKubernetesModules configuration;
allResources = moduleToAttrs (
evaldConfiguration.config.kubernetes.resources //
evaldConfiguration.config.kubernetes.customResources
);
filteredResources = filterResources resourceFilter allResources;
allDependencies = flatten (
mapAttrsToList (groupName: resources:
mapAttrsToList (name: resource: resource.nix.dependencies) resources
) filteredResources
);
resourceDependencies =
filterResources (groupName: name: resource:
elem "${groupName}/${name}" allDependencies
) allResources;
finalResources =
if withDependencies
then recursiveUpdate resourceDependencies filteredResources
else filteredResources;
resources = removeNixOptions (
# custom resource definitions have to be allways created first
(flattenResources (filterResources (groupName: name: resource:
groupName == "customResourceDefinitions"
) finalResources)) ++
# everything but custom resource definitions
(flattenResources (filterResources (groupName: name: resource:
groupName != "customResourceDefinitions"
) finalResources))
);
kubernetesList = toKubernetesList resources;
in pkgs.writeText "resources.json" (builtins.toJSON kubernetesList);
in {
inherit buildResources;
test = buildResources { configuration = ./test/default.nix; };
}

View file

@ -0,0 +1,23 @@
{
kubernetes.resources.deployments.nginx = {
metadata.labels.app = "nginx";
spec = {
replicas = 3;
selector.matchLabels.app = "nginx";
template = {
metadata.labels.app = "nginx";
spec.containers.nginx = {
name = "nginx";
image = "nginx:1.7.9";
ports."80" = {};
resources.requests.cpu = "100m";
};
};
};
};
kubernetes.resources.services.nginx = {
spec.selector.app = "nginx";
spec.ports."80".targetPort = 80;
};
}

333
kubernetes.nix Normal file
View file

@ -0,0 +1,333 @@
{ config, lib, k8s, pkgs, ... }:
with lib;
with import ./lib.nix { inherit pkgs; inherit (pkgs) lib; };
let
fixJSON = content: replaceStrings ["\\u"] ["u"] content;
fetchSpecs = path: builtins.fromJSON (fixJSON (builtins.readFile path));
hasTypeMapping = def:
hasAttr "type" def &&
elem def.type ["string" "integer" "boolean" "object"];
mapType = def:
if def.type == "string" then
if hasAttr "format" def && def.format == "int-or-string"
then types.either types.int types.str
else types.str
else if def.type == "integer" then types.int
else if def.type == "boolean" then types.bool
else if def.type == "object" then types.attrs
else throw "type ${def.type} not supported";
# Either value of type `finalType` or `coercedType`, the latter is
# converted to `finalType` using `coerceFunc`.
coercedTo = coercedType: coerceFunc: finalType:
mkOptionType rec {
name = "coercedTo";
description = "${finalType.description} or ${coercedType.description}";
check = x: finalType.check x || coercedType.check x;
merge = loc: defs:
let
coerceVal = val:
if finalType.check val then val
else let
coerced = coerceFunc val;
in assert finalType.check coerced; coerced;
in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
getSubOptions = finalType.getSubOptions;
getSubModules = finalType.getSubModules;
substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
typeMerge = t1: t2: null;
functor = (defaultFunctor name) // { wrapped = finalType; };
};
submoduleOf = definition: types.submodule ({name, ...}: {
options = definition.options;
config = definition.config;
});
refType = attr: head (tail (tail (splitString "/" attr."$ref")));
extraOptions = {
nix.dependencies = mkOption {
description = "List of resources that resource depends on";
type = types.listOf types.str;
default = [];
};
};
definitionsForKubernetesSpecs = path:
let
swagger = fetchSpecs path;
swaggerDefinitions = swagger.definitions;
definitions = mapAttrs (name: definition:
# if $ref is in definition it means it's an alias of other definition
if hasAttr "$ref" definition
then definitions."${refType definition}"
else if !(hasAttr "properties" definition)
then {}
# in other case it's an actual definition
else {
options = mapAttrs (propName: property:
let
isRequired = elem propName (definition.required or []);
requiredOrNot = type: if isRequired then type else types.nullOr type;
optionProperties =
# if $ref is in property it references other definition,
# but if other definition does not have properties, then just take it's type
if hasAttr "$ref" property then
if hasTypeMapping swaggerDefinitions.${refType property} then {
type = requiredOrNot (mapType swaggerDefinitions.${refType property});
}
else {
type = requiredOrNot (submoduleOf definitions.${refType property});
}
# if property has an array type
else if property.type == "array" then
# if reference is in items it can reference other type of another
# definition
if hasAttr "$ref" property.items then
# if it is a reference to simple type
if hasTypeMapping swaggerDefinitions.${refType property.items}
then {
type = requiredOrNot (types.listOf (mapType swaggerDefinitions.${refType property.items}.type));
}
# if a reference is to complex type
else
# if x-kubernetes-patch-merge-key is set then make it an
# attribute set of submodules
if hasAttr "x-kubernetes-patch-merge-key" property
then let
mergeKey = property."x-kubernetes-patch-merge-key";
convertName = name:
if definitions.${refType property.items}.options.${mergeKey}.type == types.int
then toInt name
else name;
in {
type = requiredOrNot (coercedTo
(types.listOf (submoduleOf definitions.${refType property.items}))
(values:
listToAttrs (map
(value: nameValuePair (
if isAttrs value.${mergeKey}
then toString value.${mergeKey}.content
else (toString value.${mergeKey})
) value)
values)
)
(types.attrsOf (types.submodule (
{name, ...}: {
options = definitions.${refType property.items}.options;
config = definitions.${refType property.items}.config // {
${mergeKey} = mkOverride 1002 (convertName name);
};
}
))
));
apply = values: if values != null then mapAttrsToList (n: v: v) values else values;
}
# in other case it's a simple list
else {
type = requiredOrNot (types.listOf (submoduleOf definitions.${refType property.items}));
}
# in other case it only references a simple type
else {
type = requiredOrNot (types.listOf (mapType property.items));
}
else if property.type == "object" && hasAttr "additionalProperties" property
then
# if it is a reference to simple type
if (
hasAttr "$ref" property.additionalProperties &&
hasTypeMapping swaggerDefinitions.${refType property.additionalProperties}
) then {
type = requiredOrNot (types.attrsOf (mapType swaggerDefinitions.${refType property.additionalProperties}));
}
# if is an array
else if property.additionalProperties.type == "array"
then {
type = requiredOrNot (types.loaOf (mapType property.additionalProperties.items));
}
else {
type = requiredOrNot (types.attrsOf (mapType property.additionalProperties));
}
# just a simple property
else {
type = requiredOrNot (mapType property);
};
in
mkOption {
inherit (definition) description;
} // optionProperties // (optionalAttrs (!isRequired) {
})
) definition.properties;
config =
let
optionalProps = filterAttrs (propName: property:
!(elem propName (definition.required or []))
) definition.properties;
in mapAttrs (name: property: mkOverride 1002 null) optionalProps;
}
) swaggerDefinitions;
exportedDefinitions =
zipAttrs (
mapAttrsToList (name: path: let
kind = path.post."x-kubernetes-group-version-kind".kind;
lastChar = substring ((stringLength kind)-1) (stringLength kind) kind;
suffix =
if lastChar == "y" then "ies"
else if hasSuffix "ss" kind then "ses"
else if lastChar == "s" then "s"
else "${lastChar}s";
optionName = "${toLower (substring 0 1 kind)}${substring 1 ((stringLength kind)-2) kind}${suffix}";
in {
${optionName} = refType (head path.post.parameters).schema;
})
(filterAttrs (name: path:
hasAttr "post" path &&
path.post."x-kubernetes-action" == "post"
) swagger.paths)
);
kubernetesResourceOptions = mapAttrs (groupName: value:
let
values = if isList value then reverseList value else [value];
definitionName = tail values;
submoduleWithDefaultsOf = definition: swaggerDefinition: let
kind = (head swaggerDefinition."x-kubernetes-group-version-kind").kind;
group = (head swaggerDefinition."x-kubernetes-group-version-kind").group;
version = (head swaggerDefinition."x-kubernetes-group-version-kind").version;
groupVersion = if group != "" then "${group}/${version}" else version;
in types.submodule ({name, ...}: {
options = definition.options // extraOptions;
config = mkMerge [
definition.config
{
kind = mkOptionDefault kind;
apiVersion = mkOptionDefault groupVersion;
# metdata.name cannot use option default, due deep config
metadata.name = mkOptionDefault name;
}
(mkAllDefault config.kubernetes.defaults.${groupName} 1001)
];
});
type =
if (length values) > 1
then fold (name: other:
types.either (submoduleWithDefaultsOf definitions.${name} swaggerDefinitions.${name}) other
) (submoduleWithDefaultsOf definitions.${head values} swaggerDefinitions.${head values}) (drop 1 values)
else submoduleWithDefaultsOf definitions.${head values} swaggerDefinitions.${head values};
in mkOption {
description = swaggerDefinitions.${definitionName}.description;
type = types.attrsOf type;
default = {};
}) exportedDefinitions;
customResourceOptions = mapAttrs (name: crd:
mkOption {
type = types.attrsOf (types.submodule ({name, config, ...}: {
options = {
apiVersion = mkOption {
description = "API version of custom resource";
type = types.str;
default = "${crd.spec.group}/${crd.spec.version}";
};
kind = mkOption {
description = "Custom resource kind";
type = types.str;
default = crd.spec.names.kind;
};
metadata = mkOption {
description = "Metadata";
type = submoduleOf definitions."io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta";
default = {};
};
spec = mkOption {
description = "Custom resource specification";
type = types.attrs;
default = {};
};
} // extraOptions;
}));
}
) config.kubernetes.resources.customResourceDefinitions;
in {
inherit swaggerDefinitions definitions exportedDefinitions kubernetesResourceOptions customResourceOptions;
};
versionDefinitions = {
"1.7" = definitionsForKubernetesSpecs ./specs/1.7/swagger.json;
"1.8" = definitionsForKubernetesSpecs ./specs/1.8/swagger.json;
};
versionOptions = {
"1.7" = (versionDefinitions."1.7").kubernetesResourceOptions // {
# kubernetes 1.7 supports crd, but does not have swagger definitions for some reason
customResourceDefinitions =
versionDefinitions."1.8".kubernetesResourceOptions.customResourceDefinitions;
};
"1.8" = (versionDefinitions."1.8").kubernetesResourceOptions;
};
defaultOptions = mapAttrs (name: value: mkOption {
type = types.attrs;
default = {};
}) versionOptions.${config.kubernetes.version};
in {
options.kubernetes.version = mkOption {
description = "Kubernetes version to deploy to";
type = types.enum (attrNames versionDefinitions);
default = "1.7";
};
options.kubernetes.resources = mkOption {
type = types.submodule {
options = versionOptions.${config.kubernetes.version};
};
description = "Attribute set of kubernetes resources";
default = {};
};
options.kubernetes.defaults = mkOption {
type = types.submodule {
options = defaultOptions;
};
description = "";
default = {};
};
options.kubernetes.customResources = mkOption {
type = types.submodule {
options = versionDefinitions.${config.kubernetes.version}.customResourceOptions;
};
description = "Attribute set of custom kubernetes resources";
default = {};
};
}

32
lib.nix Normal file
View file

@ -0,0 +1,32 @@
{lib, pkgs}:
with lib;
rec {
mkAllDefault = value: priority:
if isAttrs value
then mapAttrs (n: v: mkAllDefault v priority) value
else if isList value
then map (v: mkAllDefault v priority) value
else mkOverride priority value;
moduleToAttrs = value:
if isAttrs value
then mapAttrs (n: v: moduleToAttrs v) (filterAttrs (n: v: !(hasPrefix "_" n) && v != null) value)
else if isList value
then map (v: moduleToAttrs v) value
else value;
loadJSON = path: mkAllDefault (builtins.fromJSON (builtins.readFile path)) 1000;
loadYAML = path: loadJSON (pkgs.runCommand "yaml-to-json" {
} "${pkgs.remarshal}/bin/remarshal -i ${path} -if yaml -of json > $out");
toBase64 = value:
builtins.readFile
(pkgs.runCommand "value-to-b64" {} "echo '${value}' | ${pkgs.coreutils}/bin/base64 -w0 > $out");
}

95
modules.nix Normal file
View file

@ -0,0 +1,95 @@
{ config, lib, pkgs, k8s, ... }:
with lib;
with import ./lib.nix { inherit pkgs lib; };
let
globalConfig = config;
evalK8SModule = {module, name, configuration}: evalModules {
modules = [
./kubernetes.nix ./modules.nix module configuration
] ++ config.kubernetes.defaultModuleConfiguration;
args = {
inherit pkgs k8s name;
};
};
prefixResources = resources: serviceName:
mapAttrs (groupName: resources:
mapAttrs' (name: resource: nameValuePair "${serviceName}-${name}" resource) resources
) resources;
in {
options.kubernetes.defaultModuleConfiguration = mkOption {
description = "Default configuration for kubernetes modules";
type = types.listOf types.attrs;
default = {};
};
options.kubernetes.moduleDefinitions = mkOption {
description = "Attribute set of module definitions";
default = {};
type = types.attrsOf (types.submodule ({name, ...}: {
options = {
name = mkOption {
description = "Module definition name";
type = types.str;
default = name;
};
module = mkOption {
description = "Module definition";
};
};
}));
};
options.kubernetes.modules = mkOption {
description = "Attribute set of module definitions";
default = {};
type = types.attrsOf (types.submodule ({config, name, ...}: {
options = {
name = mkOption {
description = "Module name";
type = types.str;
default = name;
};
configuration = mkOption {
description = "Module configuration";
type = types.attrs;
default = {};
};
module = mkOption {
description = "Name of the module to use";
type = types.str;
};
evaledModule = mkOption {
description = "Evaluated config";
internal = true;
};
};
config = {
evaledModule = (evalK8SModule {
module = globalConfig.kubernetes.moduleDefinitions.${config.module}.module;
inherit (config) name configuration;
});
};
}));
};
config = {
kubernetes.resources = mkMerge (
mapAttrsToList (name: module:
prefixResources (moduleToAttrs module.evaledModule.config.kubernetes.resources) module.name
) config.kubernetes.modules
);
kubernetes.defaultModuleConfiguration = [{
config.kubernetes.version = mkDefault config.kubernetes.version;
}];
};
}

56391
specs/1.7/swagger.json Normal file

File diff suppressed because it is too large Load diff

73741
specs/1.8/swagger.json Normal file

File diff suppressed because it is too large Load diff

8
test/configMap.json Normal file
View file

@ -0,0 +1,8 @@
{
"apiVersion": "v1",
"data": {
"game.properties": "enemies=aliens\nlives=3\nenemies.cheat=true\nenemies.cheat.level=noGoodRotten\nsecret.code.passphrase=UUDDLRLRBABAS\nsecret.code.allowed=true\nsecret.code.lives=30\n",
"ui.properties": "color.good=purple\ncolor.bad=yellow\nallow.textmode=true\nhow.nice.to.look=fairlyNice\n"
},
"kind": "ConfigMap"
}

11
test/cr.json Normal file
View file

@ -0,0 +1,11 @@
{
"apiVersion": "stable.example.com/v1",
"kind": "CronTab",
"metadata": {
"name": "my-new-cron-object"
},
"spec": {
"cronSpec": "* * * * */5",
"image": "my-awesome-cron-image"
}
}

20
test/crd.json Normal file
View file

@ -0,0 +1,20 @@
{
"apiVersion": "apiextensions.k8s.io/v1beta1",
"kind": "CustomResourceDefinition",
"metadata": {
"name": "crontabs.stable.example.com"
},
"spec": {
"group": "stable.example.com",
"version": "v1",
"scope": "Namespaced",
"names": {
"plural": "crontabs",
"singular": "crontab",
"kind": "CronTab",
"shortNames": [
"ct"
]
}
}
}

67
test/daemonset.json Normal file
View file

@ -0,0 +1,67 @@
{
"kind": "DaemonSet",
"metadata": {
"labels": {
"k8s-app": "fluentd-logging"
},
"name": "fluentd-elasticsearch",
"namespace": "kube-system"
},
"spec": {
"selector": {
"matchLabels": {
"name": "fluentd-elasticsearch"
}
},
"template": {
"metadata": {
"labels": {
"name": "fluentd-elasticsearch"
}
},
"spec": {
"containers": [
{
"image": "gcr.io/google-containers/fluentd-elasticsearch:1.20",
"name": "fluentd-elasticsearch",
"resources": {
"limits": {
"memory": "200Mi"
},
"requests": {
"cpu": "100m",
"memory": "200Mi"
}
},
"volumeMounts": [
{
"mountPath": "/var/log",
"name": "varlog"
},
{
"mountPath": "/var/lib/docker/containers",
"name": "varlibdockercontainers",
"readOnly": true
}
]
}
],
"terminationGracePeriodSeconds": 30,
"volumes": [
{
"hostPath": {
"path": "/var/log"
},
"name": "varlog"
},
{
"hostPath": {
"path": "/var/lib/docker/containers"
},
"name": "varlibdockercontainers"
}
]
}
}
}
}

7
test/default.nix Normal file
View file

@ -0,0 +1,7 @@
{ config, ... }:
{
kubernetes.version = "1.7";
require = [./modules.nix ./deployment.nix];
}

38
test/deployment.json Normal file
View file

@ -0,0 +1,38 @@
{
"metadata": {
"name": "nginx-deployment",
"labels": {
"app": "nginx"
}
},
"spec": {
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": {
"nginx": {
"name": "nginx",
"image": "nginx:1.7.9",
"ports": {
"80": {}
},
"resources": {
"requests": {
"cpu": "100m"
}
}
}
}
}
}
}
}

24
test/deployment.nix Normal file
View file

@ -0,0 +1,24 @@
{lib, k8s, ...}:
with lib;
{
config = {
kubernetes.resources = {
deployments.deployment = mkMerge [
(k8s.loadJSON ./deployment.json)
{
metadata.name = "abcd";
nix.dependencies = ["configMaps/configmap"];
}
];
configMaps.configmap = k8s.loadJSON ./configMap.json;
namespaces.namespace = k8s.loadJSON ./namespace.json;
daemonSets.daemonset = k8s.loadJSON ./daemonset.json;
services.service = k8s.loadJSON ./service.json;
customResourceDefinitions.cron = k8s.loadJSON ./crd.json;
};
kubernetes.customResources.cron.my-awesome-cron-object = k8s.loadJSON ./cr.json;
};
}

54
test/modules.nix Normal file
View file

@ -0,0 +1,54 @@
{lib, k8s, ...}:
with lib;
{
config = {
kubernetes.moduleDefinitions.nginx.module = {name, config, ...}: {
options = {
port = mkOption {
description = "Port for nginx to listen on";
type = types.int;
default = 80;
};
};
config = {
kubernetes.resources.deployments.nginx = mkMerge [
(k8s.loadJSON ./deployment.json)
{
metadata.name = "${name}-nginx";
spec.template.spec.containers.nginx.ports."80" = {
containerPort = config.port;
};
spec.template.spec.containers.nginx.env.name.valueFrom.secretKeyRef = {
name = config.kubernetes.resources.configMaps.nginx.metadata.name;
key = "somekey";
};
}
];
kubernetes.resources.configMaps.nginx = mkMerge [
(k8s.loadJSON ./configMap.json)
{
metadata.name = mkForce "${name}-nginx";
}
];
};
};
kubernetes.modules.app-v1.module = "nginx";
kubernetes.modules.app-v2 = {
module = "nginx";
configuration.port = 8080;
};
kubernetes.resources.services.nginx = k8s.loadJSON ./service.json;
kubernetes.defaultModuleConfiguration = [{
kubernetes.defaults.deployments.spec.replicas = 3;
}];
};
}

10
test/namespace.json Normal file
View file

@ -0,0 +1,10 @@
{
"kind": "Namespace",
"apiVersion": "v1",
"metadata": {
"name": "openshift-origin",
"labels": {
"name": "openshift-origin"
}
}
}

26
test/service.json Normal file
View file

@ -0,0 +1,26 @@
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "nginx"
},
"spec": {
"selector": {
"app": "nginx"
},
"ports": [
{
"name": "http",
"protocol": "TCP",
"port": 80,
"targetPort": 80
},
{
"name": "https",
"protocol": "TCP",
"port": 443,
"targetPort": 443
}
]
}
}