Merge branch 'kubenix-2.0'

This commit is contained in:
Jaka Hudoklin 2020-01-15 12:25:31 +00:00
commit ef78b957fd
No known key found for this signature in database
GPG key ID: D1F18234B07BD6E2
85 changed files with 243643 additions and 216471 deletions

View file

@ -1,2 +1,3 @@
language: nix language: nix
script: nix-build -A tests script:
- nix eval -f ./ci.nix --arg release.e2e false test-check

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2003-2019 Jaka Hudoklin and the X-Truder contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -5,19 +5,38 @@ It will be merged into master in following weeks. For progress and features take
> Kubernetes resource builder written in nix > Kubernetes resource builder written in nix
[![Build Status](https://travis-ci.com/xtruder/kubenix.svg?branch=master)](https://travis-ci.com/xtruder/kubenix)
## About ## About
KubeNix is a kubernetes resource builder, that uses nix module system for KubeNix is a kubernetes resource builder, that uses nix module system for
definition of kubernetes resources and nix build system for building complex definition of kubernetes resources and nix build system for building complex
kubernetes resources very easily. kubernetes resources very easily.
### Features ## Development
- Loading and override of kubernetes json and yaml files ### Building tests
- Support for complex merging of kubernetes resource definitions
- No more helm stupid yaml templating, nix is a way better templating language ```shell
- Support for all kubernetes versions nix-build release.nix -A test-results --show-trace
```
**Building single e2e test**
```
nix-build release.nix -A tests.k8s-1_10.testsByName.k8s-crd.test
nix-build release.nix -A tests.k8s-1_10.testsByName.<test-name>.test
```
**Debugging e2e test**
```
nix-build release.nix -A tests.k8s-1_10.testsByName.k8s-crd.test.driver
nix-build release.nix -A tests.k8s-1_10.testsByName.<test-name>.test.driver
resut/bin/nixos-test-driver
testScript;
```
## License ## License
MIT © [Jaka Hudoklin](https://x-truder.net) [MIT](LICENSE) © [Jaka Hudoklin](https://x-truder.net)

11
ci.nix Normal file
View file

@ -0,0 +1,11 @@
let
nixpkgsSrc = builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/master.tar.gz";
pkgs = import nixpkgsSrc {};
lib = pkgs.lib;
release = import ./release.nix {
inherit pkgs lib;
nixosPath = "${nixpkgsSrc}/nixos";
};
in pkgs.recurseIntoAttrs release

View file

@ -1,126 +1,57 @@
{ { pkgs ? import <nixpkgs> {}, nixosPath ? toString <nixpkgs/nixos>, lib ? pkgs.lib }:
pkgs ? import <nixpkgs> {}
}:
with pkgs.lib; with lib;
with import ./lib.nix { inherit pkgs; inherit (pkgs) lib; };
let let
evalKubernetesModules = configuration: evalModules rec { kubenixLib = import ./lib { inherit lib pkgs; };
modules = [ lib' = lib.extend (lib: self: import ./lib/extra.nix { inherit lib pkgs; });
./kubernetes.nix
./modules.nix configuration defaultSpecialArgs = {
]; inherit kubenix nixosPath;
};
# evalModules with same interface as lib.evalModules and kubenix as
# special argument
evalModules = {
module ? null,
modules ? [module],
specialArgs ? defaultSpecialArgs, ...
}@attrs: let
attrs' = filterAttrs (n: _: n != "module") attrs;
in lib'.evalModules (recursiveUpdate {
inherit specialArgs modules;
args = { args = {
inherit pkgs; inherit pkgs;
name = "default"; name = "default";
k8s = import ./k8s.nix {
inherit pkgs;
inherit (pkgs) lib;
};
module = null;
};
}; };
} attrs');
flattenResources = resources: flatten ( modules = import ./modules;
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;
# legacy support for buildResources
buildResources = { buildResources = {
configuration ? {}, configuration ? {},
resourceFilter ? groupName: name: resource: true,
withDependencies ? true,
writeJSON ? true, writeJSON ? true,
writeHash ? true writeHash ? true
}: let }: let
evaldConfiguration = evalKubernetesModules configuration; evaled = evalModules {
modules = [
allResources = moduleToAttrs ( configuration
evaldConfiguration.config.kubernetes.resources // modules.legacy
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 = unique (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;
listHash = builtins.hashString "sha1" (builtins.toJSON kubernetesList);
hashedList = kubernetesList // optionalAttrs (writeHash) {
labels."kubenix/build" = listHash;
items = map (resource: recursiveUpdate resource {
metadata.labels."kubenix/build" = listHash;
}) kubernetesList.items;
}; };
result = if writeJSON then generated = evaled.config.kubernetes.generated;
pkgs.writeText "resources.json" (builtins.toJSON hashedList)
else hashedList;
in
result;
buildTest = test: version: buildResources { result =
configuration = { if writeJSON
require = [test { then pkgs.writeText "resources.json" (builtins.toJSON generated)
config.kubernetes.version = version; else generated;
}]; in result;
kubenix = {
inherit evalModules buildResources modules;
lib = kubenixLib;
}; };
}; in kubenix
in {
inherit buildResources;
tests."k8s-1_7" = buildTest ./test/default.nix "1.7";
tests."k8s-1_8" = buildTest ./test/default.nix "1.8";
tests."k8s-1_9" = buildTest ./test/default.nix "1.9";
tests."k8s-1_10" = buildTest ./test/default.nix "1.10";
tests."k8s-1_11" = buildTest ./test/default.nix "1.11";
tests."k8s-1_12" = buildTest ./test/default.nix "1.12";
tests."k8s-1_13" = buildTest ./test/default.nix "1.13";
}

5
examples/default.nix Normal file
View file

@ -0,0 +1,5 @@
{ kubenix ? import ./.. {} }:
{
nginx-deployment = import ./nginx-deployment { inherit kubenix; };
}

View file

@ -0,0 +1,28 @@
# Example: kubernetes nginx deployment
A simple example creating kubernetes nginx deployment and associated docker
image
## Usage
### Building and applying kubernetes configuration
```
nix eval -f ./. --json result | kubectl apply -f -
```
### Building and pushing docker images
```
nix run -f ./. pushDockerImages -c copy-docker-images
```
### Running tests
Test will spawn vm with kubernetes and run test script, which checks if everyting
works as expected.
```
nix build -f ./. test-script
cat result | jq '.'
```

View file

@ -0,0 +1,40 @@
{ kubenix ? import ../.. {}, registry ? "docker.io/gatehub" }:
with kubenix.lib;
rec {
# evaluated configuration
config = (kubenix.evalModules {
modules = [
./module.nix
{ docker.registry.url = registry; }
kubenix.modules.testing
{
testing.tests = [ ./test.nix ];
testing.defaults = ({ lib, ... }: with lib; {
docker.registry.url = mkForce "";
kubernetes.version = config.kubernetes.version;
});
}
];
}).config;
# e2e test
test = config.testing.result;
# nixos test script for running the test
test-script = config.testing.testsByName.nginx-deployment.test;
# genreated kubernetes List object
generated = config.kubernetes.generated;
# JSON file you can deploy to kubernetes
result = config.kubernetes.result;
# Exported docker images
images = config.docker.export;
# script to push docker images to registry
pushDockerImages = config.docker.copyScript;
}

View file

@ -0,0 +1,18 @@
{ dockerTools, nginx }:
dockerTools.buildLayeredImage {
name = "nginx";
contents = [ nginx ];
extraCommands = ''
mkdir -p etc
chmod u+w etc
echo "nginx:x:1000:1000::/:" > etc/passwd
echo "nginx:x:1000:nginx" > etc/group
'';
config = {
Cmd = ["nginx" "-c" "/etc/nginx/nginx.conf"];
ExposedPorts = {
"80/tcp" = {};
};
};
}

View file

@ -0,0 +1,64 @@
{ config, lib, pkgs, kubenix, ... }:
with lib;
let
nginx = pkgs.callPackage ./image.nix { };
in {
imports = with kubenix.modules; [ k8s docker ];
docker.images.nginx.image = nginx;
kubernetes.resources.deployments.nginx = {
spec = {
replicas = 10;
selector.matchLabels.app = "nginx";
template = {
metadata.labels.app = "nginx";
spec = {
securityContext.fsGroup = 1000;
containers.nginx = {
image = config.docker.images.nginx.path;
imagePullPolicy = "IfNotPresent";
volumeMounts."/etc/nginx".name = "config";
volumeMounts."/var/lib/html".name = "static";
};
volumes.config.configMap.name = "nginx-config";
volumes.static.configMap.name = "nginx-static";
};
};
};
};
kubernetes.resources.configMaps.nginx-config.data."nginx.conf" = ''
user nginx nginx;
daemon off;
error_log /dev/stdout info;
pid /dev/null;
events {}
http {
access_log /dev/stdout;
server {
listen 80;
index index.html;
location / {
root /var/lib/html;
}
}
}
'';
kubernetes.resources.configMaps.nginx-static.data."index.html" = ''
<html><body><h1>Hello from NGINX</h1></body></html>
'';
kubernetes.resources.services.nginx = {
spec = {
ports = [{
name = "http";
port = 80;
}];
selector.app = "nginx";
};
};
}

View file

@ -0,0 +1,20 @@
{ config, lib, pkgs, kubenix, ... }:
with lib;
{
imports = [ kubenix.modules.test ./module.nix ];
test = {
name = "nginx-deployment";
description = "Test testing nginx deployment";
testScript = ''
$kube->waitUntilSucceeds("docker load < ${config.docker.images.nginx.image}");
$kube->waitUntilSucceeds("kubectl apply -f ${config.kubernetes.result}");
$kube->succeed("kubectl get deployment | grep -i nginx");
$kube->waitUntilSucceeds("kubectl get deployment -o go-template nginx --template={{.status.readyReplicas}} | grep 10");
$kube->waitUntilSucceeds("${pkgs.curl}/bin/curl http://nginx.default.svc.cluster.local | grep -i hello");
'';
};
}

View file

@ -1,23 +0,0 @@
{
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;
};
}

View file

@ -0,0 +1,332 @@
{ pkgs ? import <nixpkgs> {}, lib ? pkgs.lib, spec ? ./istio-schema.json }:
with lib;
let
gen = rec {
mkMerge = values: ''mkMerge [${concatMapStrings (value: "
${value}
") values}]'';
toNixString = value: if isAttrs value || isList value
then builtins.toJSON value
else if isString value
then ''"${value}"''
else if value == null
then "null"
else builtins.toString value;
removeEmptyLines = str: concatStringsSep "\n" (filter (l: (builtins.match "( |)+" l) == null) (splitString "\n" str));
mkOption = {
description ? null,
type ? null,
default ? null,
apply ? null
}: removeEmptyLines ''mkOption {
${optionalString (description != null) "description = ${builtins.toJSON description};"}
${optionalString (type != null) ''type = ${type};''}
${optionalString (default != null) ''default = ${toNixString default};''}
${optionalString (apply != null) ''apply = ${apply};''}
}'';
mkOverride = priority: value: "mkOverride ${toString priority} ${toNixString value}";
types = {
unspecified = "types.unspecified";
str = "types.str";
int = "types.int";
bool = "types.bool";
attrs = "types.attrs";
nullOr = val: "(types.nullOr ${val})";
attrsOf = val: "(types.attrsOf ${val})";
listOf = val: "(types.listOf ${val})";
coercedTo = coercedType: coerceFunc: finalType:
"(types.coercedTo ${coercedType} ${coerceFunc} ${finalType})";
either = val1: val2: "(types.either ${val1} ${val2})";
loaOf = type: "(types.loaOf ${type})";
};
hasTypeMapping = def:
hasAttr "type" def &&
elem def.type ["string" "integer" "boolean"];
mergeValuesByKey = mergeKey: ''(mergeValuesByKey "${mergeKey}")'';
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 == "number" 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";
submoduleOf = definitions: ref: ''(submoduleOf "${ref}")'';
submoduleForDefinition = ref: name: kind: group: version:
''(submoduleForDefinition "${ref}" "${name}" "${kind}" "${group}" "${version}")'';
coerceAttrsOfSubmodulesToListByKey = ref: mergeKey:
''(coerceAttrsOfSubmodulesToListByKey "${ref}" "${mergeKey}")'';
attrsToList = "values: if values != null then mapAttrsToList (n: v: v) values else values";
refDefinition = attr: head (tail (tail (splitString "/" attr."$ref")));
};
fixJSON = content: replaceStrings ["\\u"] ["u"] content;
fetchSpecs = path: builtins.fromJSON (fixJSON (builtins.readFile path));
genDefinitions = swagger: with gen; (mapAttrs (name: definition:
# if $ref is in definition it means it's an alias of other definition
if hasAttr "$ref" definition
then definitions."${refDefinition definition}"
else if !(hasAttr "properties" definition)
then {
type = mapType 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 swagger.definitions.${refDefinition property} then {
type = requiredOrNot (mapType swagger.definitions.${refDefinition property});
}
else {
type = requiredOrNot (submoduleOf definitions (refDefinition property));
}
else if !(hasAttr "type" property) then {
type = types.unspecified;
}
# 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 swagger.definitions.${refDefinition property.items}
then {
type = requiredOrNot (types.listOf (mapType swagger.definitions.${refDefinition 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";
in {
type = requiredOrNot (coerceAttrsOfSubmodulesToListByKey (refDefinition property.items) mergeKey);
apply = attrsToList;
}
# in other case it's a simple list
else {
type = requiredOrNot (types.listOf (submoduleOf definitions (refDefinition 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 swagger.definitions.${refDefinition property.additionalProperties}
) then {
type = requiredOrNot (types.attrsOf (mapType swagger.definitions.${refDefinition property.additionalProperties}));
}
else if hasAttr "$ref" property.additionalProperties
then {
type = requiredOrNot types.attrs;
}
# 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 ({
description = property.description or "";
} // optionProperties)
) definition.properties;
config =
let
optionalProps = filterAttrs (propName: property:
!(elem propName (definition.required or []))
) definition.properties;
in mapAttrs (name: property: mkOverride 1002 null) optionalProps;
}
) swagger.definitions);
genResources = swagger: (mapAttrsToList (name: property: rec {
splittedType = splitString "." (removePrefix "me.snowdrop.istio.api." property.javaType);
group = (concatStringsSep "." (take ((length splittedType) - 2) splittedType)) + ".istio.io";
kind = removeSuffix "Spec" (last splittedType);
version = last (take ((length splittedType) - 1) splittedType);
ref = removePrefix "#/definitions/" property."$ref";
})
(filterAttrs (name: property:
(hasPrefix "me.snowdrop.istio.api" property.javaType) &&
hasSuffix "Spec" property.javaType
) swagger.properties)) ++ (mapAttrsToList (name: property: rec {
splittedType = splitString "." (removePrefix "me.snowdrop.istio.mixer." property.javaType);
group = "config.istio.io";
version = "v1alpha2";
kind = head (tail splittedType);
ref = removePrefix "#/definitions/" property."$ref";
}) (filterAttrs (name: property:
(hasPrefix "me.snowdrop.istio.mixer" property.javaType) &&
hasSuffix "Spec" property.javaType
) swagger.properties));
swagger = fetchSpecs spec;
definitions = genDefinitions swagger;
in pkgs.writeText "gen.nix"
"# This file was generated with kubenix k8s generator, do not edit
{lib, config, ... }:
with lib;
let
types = lib.types // rec {
str = mkOptionType {
name = \"str\";
description = \"string\";
check = isString;
merge = mergeEqualOption;
};
# 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; };
};
};
mkOptionDefault = mkOverride 1001;
extraOptions = {
kubenix = {};
};
mergeValuesByKey = mergeKey: values:
listToAttrs (map
(value: nameValuePair (
if isAttrs value.\${mergeKey}
then toString value.\${mergeKey}.content
else (toString value.\${mergeKey})
) value)
values);
submoduleOf = ref: types.submodule ({name, ...}: {
options = definitions.\"\${ref}\".options;
config = definitions.\"\${ref}\".config;
});
submoduleWithMergeOf = ref: mergeKey: types.submodule ({name, ...}: let
convertName = name:
if definitions.\"\${ref}\".options.\${mergeKey}.type == types.int
then toInt name
else name;
in {
options = definitions.\"\${ref}\".options;
config = definitions.\"\${ref}\".config // {
\${mergeKey} = mkOverride 1002 (convertName name);
};
});
submoduleForDefinition = ref: resource: kind: group: version:
types.submodule ({name, ...}: {
options = definitions.\"\${ref}\".options // extraOptions;
config = mkMerge ([
definitions.\"\${ref}\".config
{
kind = mkOptionDefault kind;
apiVersion = mkOptionDefault version;
# metdata.name cannot use option default, due deep config
metadata.name = mkOptionDefault name;
}
] ++ (config.defaults.\${resource} or [])
++ (config.defaults.all or []));
});
coerceAttrsOfSubmodulesToListByKey = ref: mergeKey: (types.coercedTo
(types.listOf (submoduleOf ref))
(mergeValuesByKey mergeKey)
(types.attrsOf (submoduleWithMergeOf ref mergeKey))
);
definitions = {
${concatStrings (mapAttrsToList (name: value: "
\"${name}\" = {${optionalString (hasAttr "options" value) "
options = {${concatStrings (mapAttrsToList (name: value: "
\"${name}\" = ${value};
") value.options)}};
"}${optionalString (hasAttr "config" value) "
config = {${concatStrings (mapAttrsToList (name: value: "
\"${name}\" = ${value};
") value.config)}};
"}};
") definitions)}
} // (import ./overrides.nix {inheirt definitions lib;}));
in {
kubernetes.customResources = [
${concatMapStrings (resource: ''{
group = "${resource.group}";
version = "${resource.version}";
kind = "${resource.kind}";
description = "";
module = definitions."${resource.ref}";
}'') (genResources swagger)}
];
}
"

File diff suppressed because it is too large Load diff

423
generators/k8s/default.nix Normal file
View file

@ -0,0 +1,423 @@
{ name ? "k8s"
, pkgs ? import <nixpkgs> {}
, lib ? pkgs.lib
, spec ? ./specs/1.10/swagger.json
, ... }:
with lib;
let
gen = rec {
mkMerge = values: ''mkMerge [${concatMapStrings (value: "
${value}
") values}]'';
toNixString = value: if isAttrs value || isList value
then builtins.toJSON value
else if isString value
then ''"${value}"''
else if value == null
then "null"
else builtins.toString value;
removeEmptyLines = str: concatStringsSep "\n" (filter (l: (builtins.match "( |)+" l) == null) (splitString "\n" str));
mkOption = {
description ? null,
type ? null,
default ? null,
apply ? null
}: removeEmptyLines ''mkOption {
${optionalString (description != null) "description = ${builtins.toJSON description};"}
${optionalString (type != null) ''type = ${type};''}
${optionalString (default != null) ''default = ${toNixString default};''}
${optionalString (apply != null) ''apply = ${apply};''}
}'';
mkOverride = priority: value: "mkOverride ${toString priority} ${toNixString value}";
types = {
unspecified = "types.unspecified";
str = "types.str";
int = "types.int";
bool = "types.bool";
attrs = "types.attrs";
nullOr = val: "(types.nullOr ${val})";
attrsOf = val: "(types.attrsOf ${val})";
listOf = val: "(types.listOf ${val})";
coercedTo = coercedType: coerceFunc: finalType:
"(types.coercedTo ${coercedType} ${coerceFunc} ${finalType})";
either = val1: val2: "(types.either ${val1} ${val2})";
loaOf = type: "(types.loaOf ${type})";
};
hasTypeMapping = def:
hasAttr "type" def &&
elem def.type ["string" "integer" "boolean"];
mergeValuesByKey = mergeKey: ''(mergeValuesByKey "${mergeKey}")'';
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 == "number" 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";
submoduleOf = definitions: ref: ''(submoduleOf "${ref}")'';
submoduleForDefinition = ref: name: kind: group: version:
''(submoduleForDefinition "${ref}" "${name}" "${kind}" "${group}" "${version}")'';
coerceAttrsOfSubmodulesToListByKey = ref: mergeKey:
''(coerceAttrsOfSubmodulesToListByKey "${ref}" "${mergeKey}")'';
attrsToList = "values: if values != null then mapAttrsToList (n: v: v) values else values";
refDefinition = attr: head (tail (tail (splitString "/" attr."$ref")));
};
refType = attr: head (tail (tail (splitString "/" attr."$ref")));
compareVersions = ver1: ver2: let
getVersion = v: substring 1 10 v;
splitVersion = v: builtins.splitVersion (getVersion v);
isAlpha = v: elem "alpha" (splitVersion v);
patchVersion = v:
if isAlpha v then ""
else if length (splitVersion v) == 1 then "${getVersion v}prod"
else getVersion v;
v1 = patchVersion ver1;
v2 = patchVersion ver2;
in builtins.compareVersions v1 v2;
fixJSON = content: replaceStrings ["\\u"] ["u"] content;
fetchSpecs = path: builtins.fromJSON (fixJSON (builtins.readFile path));
genDefinitions = swagger: with gen; mapAttrs (name: definition:
# if $ref is in definition it means it's an alias of other definition
if hasAttr "$ref" definition
then definitions."${refDefinition 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 swagger.definitions.${refDefinition property} then {
type = requiredOrNot (mapType swagger.definitions.${refDefinition property});
}
else {
type = requiredOrNot (submoduleOf definitions (refDefinition 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 swagger.definitions.${refDefinition property.items}
then {
type = requiredOrNot (types.listOf (mapType swagger.definitions.${refDefinition 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";
in {
type = requiredOrNot (coerceAttrsOfSubmodulesToListByKey (refDefinition property.items) mergeKey);
apply = attrsToList;
}
# in other case it's a simple list
else {
type = requiredOrNot (types.listOf (submoduleOf definitions (refDefinition 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 swagger.definitions.${refDefinition property.additionalProperties}
) then {
type = requiredOrNot (types.attrsOf (mapType swagger.definitions.${refDefinition property.additionalProperties}));
}
else if hasAttr "$ref" property.additionalProperties
then {
type = requiredOrNot types.attrs;
}
# 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 ({
description = property.description or "";
} // optionProperties)
) definition.properties;
config =
let
optionalProps = filterAttrs (propName: property:
!(elem propName (definition.required or []))
) definition.properties;
in mapAttrs (name: property: mkOverride 1002 null) optionalProps;
}
) swagger.definitions;
mapCharPairs = f: s1: s2: concatStrings (imap0 (i: c1:
f i c1 (if i >= stringLength s2 then "" else elemAt (stringToCharacters s2) i)
) (stringToCharacters s1));
getAttrName = resource: kind:
mapCharPairs (i: c1: c2:
if hasPrefix "API" kind && i == 0 then "A"
else if i == 0 then c1
else if c2 == "" || (toLower c2) != c1 then c1
else c2
) resource kind;
genResourceTypes = swagger: mapAttrs' (name: path: let
ref = refType (head path.post.parameters).schema;
group' = path.post."x-kubernetes-group-version-kind".group;
version' = path.post."x-kubernetes-group-version-kind".version;
kind' = path.post."x-kubernetes-group-version-kind".kind;
name' = last (splitString "/" name);
attrName = getAttrName name' kind';
in nameValuePair ref {
inherit ref attrName;
name = name';
group = if group' == "" then "core" else group';
version = version';
kind = kind';
description = swagger.definitions.${ref}.description;
defintion = refDefinition (head path.post.parameters).schema;
})
(filterAttrs (name: path:
hasAttr "post" path &&
path.post."x-kubernetes-action" == "post"
) swagger.paths);
swagger = fetchSpecs spec;
definitions = genDefinitions swagger;
resourceTypes = genResourceTypes swagger;
resourceTypesByKind = zipAttrs (mapAttrsToList (name: resourceType: {
${resourceType.kind} = resourceType;
}) resourceTypes);
resourcesTypesByKindSortByVersion = mapAttrs (kind: resourceTypes:
reverseList (sort (r1: r2:
compareVersions r1.version r2.version > 0
) resourceTypes)
) resourceTypesByKind;
latestResourceTypesByKind =
mapAttrs (kind: resources: last resources) resourcesTypesByKindSortByVersion;
genResourceOptions = resource: with gen; let
submoduleForDefinition' = definition: let
in submoduleForDefinition
definition.ref definition.name definition.kind definition.group definition.version;
in mkOption {
description = resource.description;
type = types.attrsOf (submoduleForDefinition' resource);
default = {};
};
generated = ''
# This file was generated with kubenix k8s generator, do not edit
{ lib, options, config, ... }:
with lib;
let
getDefaults = resource: group: version: kind:
catAttrs "default" (filter (default:
(default.resource == null || default.resource == resource) &&
(default.group == null || default.group == group) &&
(default.version == null || default.version == version) &&
(default.kind == null || default.kind == kind)
) config.defaults);
types = lib.types // rec {
str = mkOptionType {
name = "str";
description = "string";
check = isString;
merge = mergeEqualOption;
};
# 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; };
};
};
mkOptionDefault = mkOverride 1001;
mergeValuesByKey = mergeKey: values:
listToAttrs (map
(value: nameValuePair (
if isAttrs value.''${mergeKey}
then toString value.''${mergeKey}.content
else (toString value.''${mergeKey})
) value)
values);
submoduleOf = ref: types.submodule ({name, ...}: {
options = definitions."''${ref}".options or {};
config = definitions."''${ref}".config or {};
});
submoduleWithMergeOf = ref: mergeKey: types.submodule ({name, ...}: let
convertName = name:
if definitions."''${ref}".options.''${mergeKey}.type == types.int
then toInt name
else name;
in {
options = definitions."''${ref}".options;
config = definitions."''${ref}".config // {
''${mergeKey} = mkOverride 1002 (convertName name);
};
});
submoduleForDefinition = ref: resource: kind: group: version: let
apiVersion = if group == "core" then version else "''${group}/''${version}";
in types.submodule ({name, ...}: {
imports = getDefaults resource group version kind;
options = definitions."''${ref}".options;
config = mkMerge [
definitions."''${ref}".config
{
kind = mkOptionDefault kind;
apiVersion = mkOptionDefault apiVersion;
# metdata.name cannot use option default, due deep config
metadata.name = mkOptionDefault name;
}
];
});
coerceAttrsOfSubmodulesToListByKey = ref: mergeKey: (types.coercedTo
(types.listOf (submoduleOf ref))
(mergeValuesByKey mergeKey)
(types.attrsOf (submoduleWithMergeOf ref mergeKey))
);
definitions = {
${concatStrings (mapAttrsToList (name: value: ''
"${name}" = {
${optionalString (hasAttr "options" value) "
options = {${concatStrings (mapAttrsToList (name: value: ''
"${name}" = ${value};
'') value.options)}};
"}
${optionalString (hasAttr "config" value) ''
config = {${concatStrings (mapAttrsToList (name: value: ''
"${name}" = ${value};
'') value.config)}};
''}
};
'') definitions)}
};
in {
# all resource versions
options = {
resources = {
${concatStrings (mapAttrsToList (_: rt: ''
"${rt.group}"."${rt.version}"."${rt.kind}" = ${genResourceOptions rt};
'') resourceTypes)}
} // {
${concatStrings (mapAttrsToList (_: rt: ''
"${rt.attrName}" = ${genResourceOptions rt};
'') latestResourceTypesByKind)}
};
};
config = {
# expose resource definitions
inherit definitions;
# register resource types
types = [${concatStrings (mapAttrsToList (_: rt: ''{
name = "${rt.name}";
group = "${rt.group}";
version = "${rt.version}";
kind = "${rt.kind}";
attrName = "${rt.attrName}";
}'') resourceTypes)}];
resources = {
${concatStrings (mapAttrsToList (_: rt: ''
"${rt.group}"."${rt.version}"."${rt.kind}" =
mkAliasDefinitions options.resources."${rt.attrName}";
'') latestResourceTypesByKind)}
};
};
}
'';
in pkgs.runCommand "k8s-${name}-gen.nix" {
buildInputs = [ pkgs.haskellPackages.nixfmt ];
} ''
cp ${builtins.toFile "k8s-${name}-gen-raw.nix" generated} $out
nixfmt -w 100 $out
''

55
k8s.nix
View file

@ -1,55 +0,0 @@
{lib, pkgs}:
with lib;
with import ./lib.nix {inherit lib pkgs;};
rec {
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");
toYAML = config: builtins.readFile (pkgs.runCommand "to-yaml" {
buildInputs = [pkgs.remarshal];
} ''
remarshal -i ${pkgs.writeText "to-json" (builtins.toJSON config)} -if json -of yaml > $out
'');
toBase64 = value:
builtins.readFile
(pkgs.runCommand "value-to-b64" {} "echo -n '${value}' | ${pkgs.coreutils}/bin/base64 -w0 > $out");
exp = base: exp: foldr (value: acc: acc * base) 1 (range 1 exp);
octalToDecimal = value:
(foldr (char: acc: {
i = acc.i + 1;
value = acc.value + (toInt char) * (exp 8 acc.i);
}) {i = 0; value = 0;} (stringToCharacters value)).value;
mkSecretOption = {...}@options: mkOption (options // {
type = types.nullOr (types.submodule {
options = {
name = mkOption {
description = "Name of the secret where secret is stored";
type = types.str;
} // optionalAttrs (hasAttr "default" options && options.default != null && hasAttr "name" options.default) {
default = options.default.name;
};
key = mkOption {
description = "Name of the key where secret is stored";
type = types.str;
} // optionalAttrs (hasAttr "default" options && options.default != null && hasAttr "key" options.default) {
default = options.default.key;
};
};
});
});
secretToEnv = value: {
valueFrom.secretKeyRef = {
inherit (value) name key;
};
};
}

View file

@ -1,370 +0,0 @@
{ 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"];
str = mkOptionType {
name = "str";
description = "string";
check = isString;
merge = mergeEqualOption;
};
mapType = def:
if def.type == "string" then
if hasAttr "format" def && def.format == "int-or-string"
then types.either types.int str
else 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")));
mkOptionDefault = mkOverride 1001;
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;
}
] ++ config.kubernetes.defaults.${groupName}
++ config.kubernetes.defaults.all);
});
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 (groupName: crd:
mkOption {
description = "Custom resource for ${groupName}";
type = types.attrsOf (types.submodule ({name, ...}: {
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 = mkMerge (
config.kubernetes.defaults.${groupName} ++
config.kubernetes.defaults.all
);
}));
default = {};
}
) 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;
"1.9" = definitionsForKubernetesSpecs ./specs/1.9/swagger.json;
"1.10" = definitionsForKubernetesSpecs ./specs/1.10/swagger.json;
"1.11" = definitionsForKubernetesSpecs ./specs/1.11/swagger.json;
"1.12" = definitionsForKubernetesSpecs ./specs/1.12/swagger.json;
"1.13" = definitionsForKubernetesSpecs ./specs/1.13/swagger.json;
};
versionOptions = {
"1.7" = versionDefinitions."1.7" // {
kubernetesResourceOptions = 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";
"1.9" = versionDefinitions."1.9";
"1.10" = versionDefinitions."1.10";
"1.11" = versionDefinitions."1.11";
"1.12" = versionDefinitions."1.12";
"1.13" = versionDefinitions."1.13";
};
defaultOptions = mapAttrs (name: value: mkOption {
description = "Kubernetes defaults for ${name} resources";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
default = [];
}) (
(versionOptions.${config.kubernetes.version}.kubernetesResourceOptions) //
(versionOptions.${config.kubernetes.version}.customResourceOptions)
);
in {
options.kubernetes.version = mkOption {
description = "Kubernetes version to deploy to";
type = types.enum (attrNames versionDefinitions);
default = "1.9";
};
options.kubernetes.resources = mkOption {
type = types.submodule {
options = versionOptions.${config.kubernetes.version}.kubernetesResourceOptions;
};
description = "Attribute set of kubernetes resources";
default = {};
};
options.kubernetes.defaults = mkOption {
type = types.submodule {
options = defaultOptions // {
all = mkOption {
description = "Kubernetes defaults for all resources";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
default = [];
};
};
};
description = "";
default = {};
};
options.kubernetes.customResources = mkOption {
type = types.submodule {
options = versionDefinitions.${config.kubernetes.version}.customResourceOptions;
};
description = "Attribute set of custom kubernetes resources";
default = {};
};
}

23
lib.nix
View file

@ -1,23 +0,0 @@
{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;
}

7
lib/default.nix Normal file
View file

@ -0,0 +1,7 @@
{ lib, pkgs }:
(import ./extra.nix { inherit pkgs lib; }) // {
k8s = import ./k8s.nix { inherit lib; };
docker = import ./docker.nix { inherit lib pkgs; };
helm = import ./helm { inherit pkgs; };
}

15
lib/docker.nix Normal file
View file

@ -0,0 +1,15 @@
{ lib, pkgs }:
with lib;
{
copyDockerImages = { images, dest, args ? "" }:
pkgs.writeScriptBin "copy-docker-images" (concatMapStrings (image: ''
#!${pkgs.bash}/bin/bash
set -e
echo "copying ${image.imageName}:${image.imageTag}"
${pkgs.skopeo}/bin/skopeo copy ${args} $@ docker-archive:${image} ${dest}/${image.imageName}:${image.imageTag}
'') images);
}

121
lib/extra.nix Normal file
View file

@ -0,0 +1,121 @@
{ lib, pkgs }:
with lib;
rec {
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;
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;
loadYAML = path: importJSON (pkgs.runCommand "yaml-to-json" {
} "${pkgs.remarshal}/bin/remarshal -i ${path} -if yaml -of json > $out");
toYAML = config: builtins.readFile (pkgs.runCommand "to-yaml" {
buildInputs = [pkgs.remarshal];
} ''
remarshal -i ${pkgs.writeText "to-json" (builtins.toJSON config)} -if json -of yaml > $out
'');
toBase64 = value:
builtins.readFile
(pkgs.runCommand "value-to-b64" {} "echo -n '${value}' | ${pkgs.coreutils}/bin/base64 -w0 > $out");
exp = base: exp: foldr (value: acc: acc * base) 1 (range 1 exp);
octalToDecimal = value: (foldr (char: acc: {
i = acc.i + 1;
value = acc.value + (toInt char) * (exp 8 acc.i);
}) {i = 0; value = 0;} (stringToCharacters value)).value;
submoduleWithSpecialArgs = opts: specialArgs:
let
opts' = toList opts;
inherit (lib.modules) evalModules;
in
mkOptionType rec {
name = "submodule";
check = x: isAttrs x || isFunction x;
merge = loc: defs:
let
coerce = def: if isFunction def then def else { config = def; };
modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs;
in (evalModules {
inherit modules specialArgs;
prefix = loc;
}).config;
getSubOptions = prefix: (evalModules
{ modules = opts'; inherit prefix specialArgs;
# This is a work-around due to the fact that some sub-modules,
# such as the one included in an attribute set, expects a "args"
# attribute to be given to the sub-module. As the option
# evaluation does not have any specific attribute name, we
# provide a default one for the documentation.
#
# This is mandatory as some option declaration might use the
# "name" attribute given as argument of the submodule and use it
# as the default of option declarations.
#
# Using lookalike unicode single angle quotation marks because
# of the docbook transformation the options receive. In all uses
# &gt; and &lt; wouldn't be encoded correctly so the encoded values
# would be used, and use of `<` and `>` would break the XML document.
# It shouldn't cause an issue since this is cosmetic for the manual.
args.name = "name";
}).options;
getSubModules = opts';
substSubModules = m: submoduleWithSpecialArgs m specialArgs;
functor = (defaultFunctor name) // {
# Merging of submodules is done as part of mergeOptionDecls, as we have to annotate
# each submodule with its location.
payload = [];
binOp = lhs: rhs: [];
};
};
coerceListOfSubmodulesToAttrs = submodule: keyFn: let
mergeValuesByFn = keyFn: values:
listToAttrs (map (value:
nameValuePair (toString (keyFn value)) value
) values);
# 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; };
};
in coercedTo
(types.listOf (types.submodule submodule))
(mergeValuesByFn keyFn)
(types.attrsOf (types.submodule submodule));
}

51
lib/helm/chart2json.nix Normal file
View file

@ -0,0 +1,51 @@
{ stdenvNoCC, lib, kubernetes-helm, gawk, remarshal, jq }:
with lib;
{
# chart to template
chart
# release name
, name
# namespace to install release into
, namespace ? null
# values to pass to chart
, values ? {}
# kubernetes version to template chart for
, kubeVersion ? null }: let
valuesJsonFile = builtins.toFile "${name}-values.json" (builtins.toJSON values);
in stdenvNoCC.mkDerivation {
name = "${name}.json";
buildCommand = ''
# template helm file and write resources to yaml
helm template "${name}" \
${optionalString (kubeVersion != null) "--api-versions ${kubeVersion}"} \
${optionalString (namespace != null) "--namespace ${namespace}"} \
${optionalString (values != {}) "-f ${valuesJsonFile}"} \
${chart} >resources.yaml
# split multy yaml file into multiple files
awk 'BEGIN{i=1}{line[i++]=$0}END{j=1;n=0; while (j<i) {if (line[j] ~ /^---/) n++; else print line[j] >>"resource-"n".yaml"; j++}}' resources.yaml
# join multiple yaml files in jsonl file
for file in ./resource-*.yaml
do
remarshal -i $file -if yaml -of json >>resources.jsonl
done
# convert jsonl file to json array, remove null values and write to $out
cat resources.jsonl | jq -Scs 'walk(
if type == "object" then
with_entries(select(.value != null))
elif type == "array" then
map(select(. != null))
else
.
end)' > $out
'';
nativeBuildInputs = [ kubernetes-helm gawk remarshal jq ];
}

6
lib/helm/default.nix Normal file
View file

@ -0,0 +1,6 @@
{ pkgs }:
{
chart2json = pkgs.callPackage ./chart2json.nix { };
fetch = pkgs.callPackage ./fetchhelm.nix { };
}

50
lib/helm/fetchhelm.nix Normal file
View file

@ -0,0 +1,50 @@
{ stdenvNoCC, lib, kubernetes-helm, cacert }:
let
cleanName = name: lib.replaceStrings ["/"] ["-"] name;
in {
# name of the chart
chart
# chart url to fetch from custom location
, chartUrl ? null
# version of the chart
, version ? null
# chart hash
, sha256
# whether to extract chart
, untar ? true
# use custom charts repo
, repo ? null
# pass --verify to helm chart
, verify ? false
# pass --devel to helm chart
, devel ? false }: stdenvNoCC.mkDerivation {
name = "${cleanName chart}-${if version == null then "dev" else version}";
buildCommand = ''
export HOME="$PWD"
helm init >/dev/null
echo "adding helm repo"
${if repo == null then "" else "helm repo add repository ${repo}"}
echo "fetching helm chart"
helm fetch -d ./chart \
${if untar then "--untar" else ""} \
${if version == null then "" else "--version ${version}"} \
${if devel then "--devel" else ""} \
${if verify then "--verify" else ""} \
${if chartUrl == null then (if repo == null then chart else "repository/${chart}") else chartUrl}
cp -r chart/*/ $out
'';
outputHashMode = "recursive";
outputHashAlgo = "sha256";
outputHash = sha256;
nativeBuildInputs = [ kubernetes-helm cacert ];
}

43
lib/helm/test.nix Normal file
View file

@ -0,0 +1,43 @@
{ pkgs ? import <nixpkgs> {} }:
let
fetchhelm = pkgs.callPackage ./fetchhelm.nix { };
chart2json = pkgs.callPackage ./chart2json.nix { };
in rec {
postgresql-chart = fetchhelm {
chart = "stable/postgresql";
version = "0.18.1";
sha256 = "1p3gfmaakxrqb4ncj6nclyfr5afv7xvcdw95c6qyazfg72h3zwjn";
};
istio-chart = fetchhelm {
chart = "istio";
version = "1.1.0";
repo = "https://storage.googleapis.com/istio-release/releases/1.1.0-rc.0/charts";
sha256 = "0ippv2914hwpsb3kkhk8d839dii5whgrhxjwhpb9vdwgji5s7yfl";
};
istio-official-chart = pkgs.fetchgit {
url = "https://github.com/fyery-chen/istio-helm";
rev = "47e235e775314daeb88a3a53689ed66c396ecd3f";
sha256 = "190sfyvhdskw6ijy8cprp6hxaazn7s7mg5ids4snshk1pfdg2q8h";
};
postgresql-json = chart2json {
name = "postgresql";
chart = postgresql-chart;
values = {
networkPolicy.enabled = true;
};
};
istio-json = chart2json {
name = "istio";
chart = istio-chart;
};
istio-official-json = chart2json {
name = "istio-official";
chart = "${istio-official-chart}/istio-official";
};
}

61
lib/k8s.nix Normal file
View file

@ -0,0 +1,61 @@
{ lib }:
with lib;
rec {
# TODO: refactor with mkOptionType
mkSecretOption = {description ? "", default ? {}, allowNull ? true}: mkOption {
inherit description;
type = (if allowNull then types.nullOr else id) (types.submodule {
options = {
name = mkOption ({
description = "Name of the secret where secret is stored";
type = types.str;
default = default.name;
} // (optionalAttrs (default ? "name") {
default = default.name;
}));
key = mkOption ({
description = "Name of the key where secret is stored";
type = types.str;
} // (optionalAttrs (default ? "key") {
default = default.key;
}));
};
});
default = if default == null then null else {};
};
secretToEnv = value: {
valueFrom.secretKeyRef = {
inherit (value) name key;
};
};
# Creates kubernetes list from a list of kubernetes objects
mkList = { items, labels ? {} }: {
kind = "List";
apiVersion = "v1";
inherit items labels;
};
# Creates hashed kubernetes list from a list of kubernetes objects
mkHashedList = { items, labels ? {} }: let
hash = builtins.hashString "sha1" (builtins.toJSON items);
labeledItems = map (item: recursiveUpdate item {
metadata.labels."kubenix/hash" = hash;
}) items;
in mkList {
items = labeledItems;
labels = {
"kubenix/hash" = hash;
} // labels;
};
toBase64 = lib.toBase64;
octalToDecimal = lib.octalToDecimal;
}

39
modules/base.nix Normal file
View file

@ -0,0 +1,39 @@
{ config, lib, ... }:
with lib;
{
options = {
kubenix.project = mkOption {
description = "Name of the project";
type = types.str;
default = "kubenix";
};
_module.features = mkOption {
description = "List of features exposed by module";
type = types.listOf types.str;
default = [];
};
_module.propagate = mkOption {
description = "Module propagation options";
type = types.listOf (types.submodule ({config, ...}: {
options = {
features = mkOption {
description = "List of features that submodule has to have to propagate module";
type = types.listOf types.str;
default = [];
};
module = mkOption {
description = "Module to propagate";
type = types.unspecified;
default = {};
};
};
}));
default = [];
};
};
}

12
modules/default.nix Normal file
View file

@ -0,0 +1,12 @@
{
k8s = ./k8s.nix;
istio = ./istio.nix;
submodules = ./submodules.nix;
submodule = ./submodule.nix;
helm = ./helm.nix;
docker = ./docker.nix;
testing = ./testing.nix;
test = ./test.nix;
module = ./module.nix;
legacy = ./legacy.nix;
}

94
modules/docker.nix Normal file
View file

@ -0,0 +1,94 @@
{ config, lib, pkgs, docker, ... }:
with lib;
let
cfg = config.docker;
in {
imports = [ ./base.nix ];
options.docker = {
registry.url = mkOption {
description = "Default registry url where images are published";
type = types.str;
default = "";
};
images = mkOption {
description = "Attribute set of docker images that should be published";
type = types.attrsOf (types.submodule ({ name, config, ... }: {
options = {
image = mkOption {
description = "Docker image to publish";
type = types.nullOr types.package;
default = null;
};
name = mkOption {
description = "Desired docker image name";
type = types.str;
default = builtins.unsafeDiscardStringContext config.image.imageName;
};
tag = mkOption {
description = "Desired docker image tag";
type = types.str;
default = builtins.unsafeDiscardStringContext config.image.imageTag;
};
registry = mkOption {
description = "Docker registry url where image is published";
type = types.str;
default = cfg.registry.url;
};
path = mkOption {
description = "Full docker image path";
type = types.str;
default =
if config.registry != ""
then "${config.registry}/${config.name}:${config.tag}"
else "${config.name}:${config.tag}";
};
};
}));
default = {};
};
export = mkOption {
description = "List of images to export";
type = types.listOf types.package;
default = [];
};
copyScript = mkOption {
description = "Image copy script";
type = types.package;
default = docker.copyDockerImages {
dest = "docker://${cfg.registry.url}";
images = cfg.export;
};
};
};
config = {
# define docker feature
_module.features = ["docker"];
# pass docker library as param
_module.args.docker = import ../lib/docker.nix { inherit lib pkgs; };
# propagate docker options if docker feature is enabled
_module.propagate = [{
features = [ "docker" ];
module = { config, name, ... }: {
# propagate registry options
docker.registry = cfg.registry;
};
}];
# list of exported docker images
docker.export = mapAttrsToList (_: i: i.image)
(filterAttrs (_: i: i.registry != null) config.docker.images);
};
}

28968
modules/generated/v1.10.nix Executable file

File diff suppressed because it is too large Load diff

29520
modules/generated/v1.11.nix Executable file

File diff suppressed because it is too large Load diff

30298
modules/generated/v1.12.nix Executable file

File diff suppressed because it is too large Load diff

30956
modules/generated/v1.13.nix Executable file

File diff suppressed because it is too large Load diff

21525
modules/generated/v1.14.nix Normal file

File diff suppressed because it is too large Load diff

21867
modules/generated/v1.15.nix Normal file

File diff suppressed because it is too large Load diff

12898
modules/generated/v1.7.nix Executable file

File diff suppressed because it is too large Load diff

25892
modules/generated/v1.8.nix Executable file

File diff suppressed because it is too large Load diff

27984
modules/generated/v1.9.nix Executable file

File diff suppressed because it is too large Load diff

109
modules/helm.nix Normal file
View file

@ -0,0 +1,109 @@
# helm defines kubenix module with options for using helm charts
# with kubenix
{ config, lib, pkgs, helm, ... }:
with lib;
let
cfg = config.kubernetes.helm;
globalConfig = config;
recursiveAttrs = mkOptionType {
name = "recursive-attrs";
description = "recursive attribute set";
check = isAttrs;
merge = loc: foldl' (res: def: recursiveUpdate res def.value) {};
};
parseApiVersion = apiVersion: let
splitted = splitString "/" apiVersion;
in {
group = if length splitted == 1 then "core" else head splitted;
version = last splitted;
};
in {
imports = [ ./k8s.nix ];
options.kubernetes.helm = {
instances = mkOption {
description = "Attribute set of helm instances";
type = types.attrsOf (types.submodule ({ config, name, ... }: {
options = {
name = mkOption {
description = "Helm release name";
type = types.str;
default = name;
};
chart = mkOption {
description = "Helm chart to use";
type = types.package;
};
namespace = mkOption {
description = "Namespace to install helm chart to";
type = types.nullOr types.str;
default = null;
};
values = mkOption {
description = "Values to pass to chart";
type = recursiveAttrs;
default = {};
};
kubeVersion = mkOption {
description = "Kubernetes version to build chart for";
type = types.str;
default = globalConfig.kubernetes.version;
};
overrides = mkOption {
description = "Overrides to apply to all chart resources";
type = types.listOf types.unspecified;
default = [];
};
overrideNamespace = mkOption {
description = "Whether to apply namespace override";
type = types.bool;
default = true;
};
objects = mkOption {
description = "Generated kubernetes objects";
type = types.listOf types.attrs;
default = [];
};
};
config.overrides = mkIf (config.overrideNamespace && config.namespace != null) [{
metadata.namespace = config.namespace;
}];
config.objects = importJSON (helm.chart2json {
inherit (config) chart name namespace values kubeVersion;
});
}));
};
};
config = {
# expose helm helper methods as module argument
_module.args.helm = import ../lib/helm { inherit pkgs; };
kubernetes.api.resources = mkMerge (flatten (mapAttrsToList (_: instance:
map (object: let
apiVersion = parseApiVersion object.apiVersion;
name = object.metadata.name;
in {
"${apiVersion.group}"."${apiVersion.version}".${object.kind}."${name}" = mkMerge ([
object
] ++ instance.overrides);
}) instance.objects
) cfg.instances));
};
}

View file

@ -0,0 +1,15 @@
{ lib, definitions }:
with lib;
{
"istio_networking_v1alpha3_StringMatch" = recursiveUpdate (recursiveUpdate
definitions."istio_networking_v1alpha3_StringMatch_Exact"
definitions."istio_networking_v1alpha3_StringMatch_Prefix"
)
definitions."istio_networking_v1alpha3_StringMatch_Regex";
"istio_networking_v1alpha3_PortSelector" = recursiveUpdate
definitions."istio_networking_v1alpha3_PortSelector_Name"
definitions."istio_networking_v1alpha3_PortSelector_Number";
}

4921
modules/istio.nix Normal file

File diff suppressed because it is too large Load diff

412
modules/k8s.nix Normal file
View file

@ -0,0 +1,412 @@
# K8S module defines kubernetes definitions for kubenix
{ options, config, lib, pkgs, k8s, ... }:
with lib;
let
cfg = config.kubernetes;
gvkKeyFn = type: "${type.group}/${type.version}/${type.kind}";
getDefaults = resource: group: version: kind:
catAttrs "default" (filter (default:
(resource == null || default.resource == null || default.resource == resource) &&
(default.group == null || default.group == group) &&
(default.version == null || default.version == version) &&
(default.kind == null || default.kind == kind)
) cfg.api.defaults);
moduleToAttrs = value:
if isAttrs value
then mapAttrs (n: v: moduleToAttrs v) (filterAttrs (n: v: v != null && !(hasPrefix "_" n)) value)
else if isList value
then map (v: moduleToAttrs v) value
else value;
apiOptions = { config, ... }: {
options = {
definitions = mkOption {
description = "Attribute set of kubernetes definitions";
};
defaults = mkOption {
description = "Kubernetes defaults to apply to resources";
type = types.listOf (types.submodule ({config, ...}: {
options = {
group = mkOption {
description = "Group to apply default to (all by default)";
type = types.nullOr types.str;
default = null;
};
version = mkOption {
description = "Version to apply default to (all by default)";
type = types.nullOr types.str;
default = null;
};
kind = mkOption {
description = "Kind to apply default to (all by default)";
type = types.nullOr types.str;
default = null;
};
resource = mkOption {
description = "Resource to apply default to (all by default)";
type = types.nullOr types.str;
default = null;
};
propagate = mkOption {
description = "Whether to propagate defaults";
type = types.bool;
default = false;
};
default = mkOption {
description = "Default to apply";
type = types.unspecified;
default = {};
};
};
}));
default = [];
apply = unique;
};
types = mkOption {
description = "List of registered kubernetes types";
type = coerceListOfSubmodulesToAttrs {
options = {
group = mkOption {
description = "Resource type group";
type = types.str;
};
version = mkOption {
description = "Resoruce type version";
type = types.str;
};
kind = mkOption {
description = "Resource type kind";
type = types.str;
};
name = mkOption {
description = "Resource type name";
type = types.nullOr types.str;
};
attrName = mkOption {
description = "Name of the nixified attribute";
type = types.str;
};
};
} gvkKeyFn;
default = {};
};
};
config = {
# apply aliased option
resources = mkAliasDefinitions options.kubernetes.resources;
};
};
indexOf = lst: value:
head (filter (v: v != -1) (imap0 (i: v: if v == value then i else -1) lst));
compareVersions = ver1: ver2: let
getVersion = v: substring 1 10 v;
splittedVer1 = builtins.splitVersion (getVersion ver1);
splittedVer2 = builtins.splitVersion (getVersion ver2);
v1 = if length splittedVer1 == 1 then "${getVersion ver1}prod" else getVersion ver1;
v2 = if length splittedVer2 == 1 then "${getVersion ver2}prod" else getVersion ver2;
in builtins.compareVersions v1 v2;
customResourceTypesByAttrName = zipAttrs (mapAttrsToList (_: resourceType: {
${resourceType.attrName} = resourceType;
}) cfg.customTypes);
customResourceTypesByAttrNameSortByVersion = mapAttrs (_: resourceTypes:
reverseList (sort (r1: r2:
compareVersions r1.version r2.version > 0
) resourceTypes)
) customResourceTypesByAttrName;
latestCustomResourceTypes =
mapAttrsToList (_: resources: last resources) customResourceTypesByAttrNameSortByVersion;
customResourceModuleForType = config: ct: { name, ... }: {
imports = getDefaults ct.name ct.group ct.version ct.kind;
options = {
apiVersion = mkOption {
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";
type = types.nullOr types.str;
};
kind = mkOption {
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";
type = types.nullOr types.str;
};
metadata = mkOption {
description = "Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata.";
type = types.nullOr (types.submodule config.definitions."io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta");
};
spec = mkOption {
description = "Module spec";
type = types.either types.attrs (types.submodule ct.module);
default = {};
};
};
config = {
apiVersion = mkOptionDefault "${ct.group}/${ct.version}";
kind = mkOptionDefault ct.kind;
metadata.name = mkDefault name;
};
};
customResourceOptions = (mapAttrsToList (_: ct: {config, ...}: let
module = customResourceModuleForType config ct;
in {
options.resources.${ct.group}.${ct.version}.${ct.kind} = mkOption {
description = ct.description;
type = types.attrsOf (types.submodule module);
default = {};
};
}) cfg.customTypes) ++ (map (ct: { options, config, ... }: let
module = customResourceModuleForType config ct;
in {
options.resources.${ct.attrName} = mkOption {
description = ct.description;
type = types.attrsOf (types.submodule module);
default = {};
};
config.resources.${ct.group}.${ct.version}.${ct.kind} =
mkAliasDefinitions options.resources.${ct.attrName};
}) latestCustomResourceTypes);
in {
imports = [ ./base.nix ];
options.kubernetes = {
version = mkOption {
description = "Kubernetes version to use";
type = types.enum ["1.7" "1.8" "1.9" "1.10" "1.11" "1.12" "1.13" "1.14" "1.15"];
default = "1.15";
};
namespace = mkOption {
description = "Default namespace where to deploy kubernetes resources";
type = types.str;
default = "default";
};
resourceOrder = mkOption {
description = "Preffered resource order";
type = types.listOf types.str;
default = [
"CustomResourceDefinition"
"Namespace"
];
};
api = mkOption {
type = types.submodule {
imports = [
(./generated + ''/v'' + cfg.version + ".nix")
apiOptions
] ++ customResourceOptions;
};
default = {};
};
imports = mkOption {
type = types.listOf (types.either types.package types.path);
description = "List of resources to import";
default = [];
};
resources = mkOption {
description = "Alias for `config.kubernetes.api.resources` options";
default = {};
type = types.attrsOf types.attrs;
};
createCustomTypesFromCRDs = mkOption {
description = "Whether to create customTypes from custom resource definitions";
type = types.bool;
default = false;
};
customTypes = mkOption {
description = "List of custom resource types to make API for";
type = coerceListOfSubmodulesToAttrs {
options = {
group = mkOption {
description = "Custom type group";
type = types.str;
};
version = mkOption {
description = "Custom type version";
type = types.str;
};
kind = mkOption {
description = "Custom type kind";
type = types.str;
};
name = mkOption {
description = "Custom type resource name";
type = types.nullOr types.str;
default = null;
};
attrName = mkOption {
description = "Name of the nixified attribute";
type = types.str;
};
description = mkOption {
description = "Custom type description";
type = types.str;
default = "";
};
module = mkOption {
description = "Custom type module";
type = types.unspecified;
default = {};
};
};
} gvkKeyFn;
default = {};
};
objects = mkOption {
description = "List of generated kubernetes objects";
type = types.listOf types.attrs;
apply = items: sort (r1: r2:
if elem r1.kind cfg.resourceOrder && elem r2.kind cfg.resourceOrder
then indexOf cfg.resourceOrder r1.kind < indexOf cfg.resourceOrder r2.kind
else if elem r1.kind cfg.resourceOrder then true else false
) (unique items);
default = [];
};
generated = mkOption {
description = "Generated kubernetes list object";
type = types.attrs;
};
result = mkOption {
description = "Generated kubernetes JSON file";
type = types.package;
};
};
config = {
# expose k8s helper methods as module argument
_module.args.k8s = import ../lib/k8s.nix { inherit lib; };
# features that module is defining
_module.features = [ "k8s" ];
# module propagation options
_module.propagate = [{
features = ["k8s"];
module = { config, ... }: {
# propagate kubernetes version and namespace
kubernetes.version = mkDefault cfg.version;
kubernetes.namespace = mkDefault cfg.namespace;
};
} {
features = ["k8s" "submodule"];
module = { config, ... }: {
# set module defaults
kubernetes.api.defaults = (
# propagate defaults if default propagation is enabled
(filter (default: default.propagate) cfg.api.defaults) ++
[
# set module name and version for all kuberentes resources
{
default.metadata.labels = {
"kubenix/module-name" = config.submodule.name;
"kubenix/module-version" = config.submodule.version;
};
}
]
);
};
}];
kubernetes.api = mkMerge ([{
# register custom types
types = mapAttrsToList (_: cr: {
inherit (cr) name group version kind attrName;
}) cfg.customTypes;
defaults = [{
default = {
# set default kubernetes namespace to all resources
metadata.namespace = mkDefault config.kubernetes.namespace;
# set project name to all resources
metadata.labels."kubenix/project-name" = config.kubenix.project;
};
}];
}] ++
# import of yaml files
(map (i: let
# load yaml file
object = loadYAML i;
groupVersion = splitString "/" object.apiVersion;
name = object.metadata.name;
version = last groupVersion;
group =
if version == (head groupVersion)
then "core" else head groupVersion;
kind = object.kind;
in {
resources.${group}.${version}.${kind}.${name} = object;
}) cfg.imports));
kubernetes.objects = flatten (mapAttrsToList (_: type:
mapAttrsToList (name: resource: moduleToAttrs resource)
cfg.api.resources.${type.group}.${type.version}.${type.kind}
) cfg.api.types);
# custom types created from customResourceDefinitions
kubernetes.customTypes = mkIf cfg.createCustomTypesFromCRDs (
mapAttrsToList (name: crd: {
group = crd.spec.group;
version = crd.spec.version;
kind = crd.spec.names.kind;
name = crd.spec.names.plural;
attrName = mkOptionDefault name;
}) (cfg.resources.customResourceDefinitions or {})
);
kubernetes.generated = k8s.mkHashedList {
items = config.kubernetes.objects;
labels."kubenix/project-name" = config.kubenix.project;
labels."kubenix/k8s-version" = config.kubernetes.version;
};
kubernetes.result =
pkgs.writeText "kubenix-generated.json" (builtins.toJSON cfg.generated);
};
}

View file

@ -1,54 +1,15 @@
{ config, options, lib, pkgs, k8s, module ? null, ... }: # support for legacy kubenix
{ options, config, pkgs, lib, kubenix, ... }:
with lib; with lib;
with import ./lib.nix { inherit pkgs lib; };
let let
globalConfig = config;
parentModule = module; parentModule = module;
globalConfig = config;
mkOptionDefault = mkOverride 1001; mkOptionDefault = mkOverride 1001;
# A submodule (like typed attribute set). See NixOS manual.
submodule = opts:
let
opts' = toList opts;
inherit (lib.modules) evalModules;
in
mkOptionType rec {
name = "submodule";
check = x: isAttrs x || isFunction x;
merge = loc: defs:
let
coerce = def: if isFunction def then def else { config = def; };
modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs;
in (evalModules {
inherit modules;
prefix = loc;
}).config;
getSubOptions = prefix: (evalModules
{ modules = opts'; inherit prefix;
# This is a work-around due to the fact that some sub-modules,
# such as the one included in an attribute set, expects a "args"
# attribute to be given to the sub-module. As the option
# evaluation does not have any specific attribute name, we
# provide a default one for the documentation.
#
# This is mandatory as some option declaration might use the
# "name" attribute given as argument of the submodule and use it
# as the default of option declarations.
args.name = "&lt;name&gt;";
}).options;
getSubModules = opts';
substSubModules = m: submodule m;
functor = (defaultFunctor name) // {
# Merging of submodules is done as part of mergeOptionDecls, as we have to annotate
# each submodule with its location.
payload = [];
binOp = lhs: rhs: [];
};
};
mkModuleOptions = moduleDefinition: module: mkModuleOptions = moduleDefinition: module:
let let
# gets file where module is defined by looking into moduleDefinitions # gets file where module is defined by looking into moduleDefinitions
@ -70,43 +31,42 @@ let
); );
in [ in [
{ {
_module.args.k8s = k8s;
_module.args.name = module.name; _module.args.name = module.name;
_module.args.module = module; _module.args.module = module;
} }
./kubernetes.nix ./k8s.nix
./modules.nix ./legacy.nix
(injectModuleAttrs moduleDefinition.module {_file = file;}) (injectModuleAttrs moduleDefinition.module {_file = file;})
{ {
config.kubernetes.defaults.all.metadata.namespace = mkOptionDefault module.namespace; config.kubernetes.namespace = mkOptionDefault module.namespace;
config.kubenix.project = mkOptionDefault config.kubenix.project;
} }
] ++ config.kubernetes.defaultModuleConfiguration.all ] ++ config.kubernetes.defaultModuleConfiguration.all
++ (optionals (hasAttr moduleDefinition.name config.kubernetes.defaultModuleConfiguration) ++ (optionals (hasAttr moduleDefinition.name config.kubernetes.defaultModuleConfiguration)
config.kubernetes.defaultModuleConfiguration.${moduleDefinition.name}); config.kubernetes.defaultModuleConfiguration.${moduleDefinition.name});
# prefix kubernetes objects with ${serviceName}, this magic was removed in new kubenix
prefixResources = resources: serviceName: prefixResources = resources: serviceName:
mapAttrs (groupName: resources: mapAttrs' (name: resource: nameValuePair "${serviceName}-${name}" resource) resources;
mapAttrs' (name: resource: nameValuePair "${serviceName}-${name}" resource) resources
) resources;
prefixGroupResources = resources: serviceName:
mapAttrs' (groupName: resources:
nameValuePair "${serviceName}-${groupName}" resources
) resources;
# TODO: rewrite using mkOptionType
defaultModuleConfigurationOptions = mapAttrs (name: moduleDefinition: mkOption { defaultModuleConfigurationOptions = mapAttrs (name: moduleDefinition: mkOption {
description = "Module default configuration for ${name} module"; description = "Module default configuration for ${name} module";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified); type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
default = []; default = [];
apply = filter (v: v!=[]);
}) config.kubernetes.moduleDefinitions; }) config.kubernetes.moduleDefinitions;
getModuleDefinition = name: getModuleDefinition = name:
if hasAttr name config.kubernetes.moduleDefinitions if hasAttr name config.kubernetes.moduleDefinitions
then config.kubernetes.moduleDefinitions.${name} then config.kubernetes.moduleDefinitions.${name}
else throw ''requested kubernetes moduleDefinition with name "${name}" does not exist''; else throw ''requested kubernetes moduleDefinition with name "${name}" does not exist'';
in { in {
imports = [ ./k8s.nix ];
options.kubernetes.moduleDefinitions = mkOption { options.kubernetes.moduleDefinitions = mkOption {
description = "Attribute set of module definitions"; description = "Legacy kubenix attribute set of module definitions";
default = {}; default = {};
type = types.attrsOf (types.submodule ({name, ...}: { type = types.attrsOf (types.submodule ({name, ...}: {
options = { options = {
@ -136,13 +96,14 @@ in {
}; };
options.kubernetes.defaultModuleConfiguration = mkOption { options.kubernetes.defaultModuleConfiguration = mkOption {
description = "Module default options"; description = "Legacy kubenix module default options";
type = types.submodule { type = types.submodule {
options = defaultModuleConfigurationOptions // { options = defaultModuleConfigurationOptions // {
all = mkOption { all = mkOption {
description = "Module default configuration for all modules"; description = "Module default configuration for all modules";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified); type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
default = []; default = [];
apply = filter (v: v != []);
}; };
}; };
}; };
@ -150,7 +111,7 @@ in {
}; };
options.kubernetes.modules = mkOption { options.kubernetes.modules = mkOption {
description = "Attribute set of modules"; description = "Legacy kubenix attribute set of modules";
default = {}; default = {};
type = types.attrsOf (types.submodule ({config, name, ...}: { type = types.attrsOf (types.submodule ({config, name, ...}: {
options = { options = {
@ -163,10 +124,7 @@ in {
namespace = mkOption { namespace = mkOption {
description = "Namespace where to deploy module"; description = "Namespace where to deploy module";
type = types.str; type = types.str;
default = default = globalConfig.kubernetes.namespace;
if parentModule != null
then parentModule.namespace
else "default";
}; };
labels = mkOption { labels = mkOption {
@ -177,8 +135,10 @@ in {
configuration = mkOption { configuration = mkOption {
description = "Module configuration"; description = "Module configuration";
type = submodule { type = submoduleWithSpecialArgs {
imports = mkModuleOptions (getModuleDefinition config.module) config; imports = mkModuleOptions (getModuleDefinition config.module) config;
} {
inherit kubenix;
}; };
default = {}; default = {};
}; };
@ -192,39 +152,54 @@ in {
})); }));
}; };
options.kubernetes.defaults = mkOption {
type = types.attrsOf (types.coercedTo types.attrs (value: [value]) (types.listOf types.attrs));
description = "Legacy kubenix kubernetes defaults.";
default = {};
};
# for back compatibility with kubernetes.customResources
options.kubernetes.customResources = options.kubernetes.resources;
config = { config = {
kubernetes.resources = mkMerge ( kubernetes = mkMerge [{
mapAttrsToList (name: module: let api.defaults = mapAttrsToList (attrName: default: let
type = head (mapAttrsToList (_: v: v) (filterAttrs (_: type: type.attrName == attrName) config.kubernetes.api.types));
in {
default = { imports = default; };
} // (if (attrName == "all") then {} else {
resource = type.name;
})) config.kubernetes.defaults;
resources = mkMerge (
mapAttrsToList (name: module:
mapAttrs' (_: type: let
moduleDefinition = getModuleDefinition module.module; moduleDefinition = getModuleDefinition module.module;
moduleResources = module.configuration.kubernetes.api.resources.${type.attrName} or {};
moduleConfig = moduleConfig =
if moduleDefinition.prefixResources if moduleDefinition.prefixResources && type.kind != "CustomResourceDefinition"
then prefixResources (moduleToAttrs module.configuration.kubernetes.resources) name then prefixResources (moduleToAttrs moduleResources) name
else moduleToAttrs module.configuration.kubernetes.resources; else moduleToAttrs moduleResources;
in in nameValuePair type.attrName
if moduleDefinition.assignAsDefaults (if moduleDefinition.assignAsDefaults
then mkAllDefault moduleConfig 1000 then mkAllDefault moduleConfig 1000
else moduleConfig else moduleConfig)
) module.configuration.kubernetes.api.types
) config.kubernetes.modules ) config.kubernetes.modules
); );
kubernetes.customResources = mkMerge ( # create custom types from CRDs was old behavior
mapAttrsToList (name: module: let createCustomTypesFromCRDs = true;
moduleDefinition = getModuleDefinition module.module;
moduleConfig =
if moduleDefinition.prefixResources
then prefixGroupResources (moduleToAttrs module.configuration.kubernetes.customResources) name
else moduleToAttrs module.configuration.kubernetes.customResources;
in
if moduleDefinition.assignAsDefaults
then mkAllDefault moduleConfig 1000
else moduleConfig
) config.kubernetes.modules
);
kubernetes.defaultModuleConfiguration.all = { defaultModuleConfiguration.all = {
_file = head options.kubernetes.defaultModuleConfiguration.files; _file = head options.kubernetes.defaultModuleConfiguration.files;
config.kubernetes.version = mkDefault config.kubernetes.version; config.kubernetes.version = mkDefault config.kubernetes.version;
config.kubernetes.moduleDefinitions = config.kubernetes.moduleDefinitions; config.kubernetes.moduleDefinitions = config.kubernetes.moduleDefinitions;
}; };
} {
resources = mkAliasDefinitions options.kubernetes.customResources;
}];
}; };
} }

49
modules/submodule.nix Normal file
View file

@ -0,0 +1,49 @@
{ config, lib, ... }:
with lib;
{
imports = [ ./base.nix ];
options.submodule = {
name = mkOption {
description = "Module name";
type = types.str;
};
description = mkOption {
description = "Module description";
type = types.str;
default = "";
};
version = mkOption {
description = "Module version";
type = types.str;
default = "1.0.0";
};
tags = mkOption {
description = "List of submodule tags";
type = types.listOf types.str;
default = [];
};
exports = mkOption {
description = "Attribute set of functions to export";
type = types.attrs;
default = {};
};
passthru = mkOption {
description = "Attribute set to passthru";
type = types.attrs;
default = {};
};
args._empty = mkOption {};
};
config._module.features = ["submodule"];
config._module.args.args = config.submodule.args;
}

290
modules/submodules.nix Normal file
View file

@ -0,0 +1,290 @@
{ config, options, kubenix, pkgs, lib, ... }:
with lib;
let
cfg = config.submodules;
parentConfig = config;
matchesVersion = requiredVersion: version:
if requiredVersion != null then
if hasPrefix "~" requiredVersion
then (builtins.match (removePrefix "~" requiredVersion) version) != null
else requiredVersion == version
else true;
getDefaults = {name, version, tags, features}:
catAttrs "default" (filter (submoduleDefault:
(submoduleDefault.name == null || submoduleDefault.name == name) &&
(matchesVersion submoduleDefault.version version) &&
(
(length submoduleDefault.tags == 0) ||
(length (intersectLists submoduleDefault.tags tags)) > 0
) &&
(
(length submoduleDefault.features == 0) ||
(length (intersectLists submoduleDefault.features features)) > 0
)
) config.submodules.defaults);
specialArgs = cfg.specialArgs // {
parentConfig = config;
};
findSubmodule = {name, version ? null, latest ? true}: let
matchingSubmodules = filter (el:
el.definition.name == name &&
(matchesVersion version el.definition.version)
) cfg.imports;
versionSortedSubmodules = sort (s1: s2:
if builtins.compareVersions s1.definition.version s2.definition.version > 0
then true else false
) matchingSubmodules;
matchingModule =
if length versionSortedSubmodules == 0
then throw "No module found ${name}/${if version == null then "latest" else version}"
else head versionSortedSubmodules;
in matchingModule;
passthruConfig = mapAttrsToList (name: opt: {
${name} = mkMerge (mapAttrsToList (_: inst:
if inst.passthru.enable
then inst.config.submodule.passthru.${name} or {}
else {}
) config.submodules.instances);
_module.args = mkMerge (mapAttrsToList (_: inst:
if inst.passthru.enable
then inst.config.submodule.passthru._module.args or {}
else {}
) config.submodules.instances);
}) (removeAttrs options ["_definedNames" "_module" "submodules"]);
in {
imports = [ ./base.nix ];
options = {
submodules.specialArgs = mkOption {
description = "Special args to pass to submodules. These arguments can be used for imports";
type = types.attrs;
default = {};
};
submodules.defaults = mkOption {
description = "List of defaults to apply to submodule instances";
type = types.listOf (types.submodule ({config, ...}: {
options = {
name = mkOption {
description = "Name of the submodule to apply defaults for";
type = types.nullOr types.str;
default = null;
};
version = mkOption {
description = ''
Version of submodule to apply defaults for. If version starts with
"~" it is threated as regex pattern for example "~1.0.*
'';
type = types.nullOr types.str;
default = null;
};
tags = mkOption {
description = "List of tags to apply defaults for";
type = types.listOf types.str;
default = [];
};
features = mkOption {
description = "List of features that submodule has to have to apply defaults";
type = types.listOf types.str;
default = [];
};
default = mkOption {
description = "Default to apply to submodule instance";
type = types.unspecified;
default = {};
};
};
}));
default = [];
};
submodules.propagate.enable = mkOption {
description = "Whether to propagate defaults and imports from parent to child";
type = types.bool;
default = true;
};
submodules.imports = mkOption {
description = "List of submodule imports";
type = types.listOf (
types.coercedTo
types.path
(module: {inherit module;})
(types.submodule ({name, config, ...}: let
evaledSubmodule' = evalModules {
inherit specialArgs;
modules = config.modules ++ [ ./base.nix ];
check = false;
};
evaledSubmodule =
if (!(elem "submodule" evaledSubmodule'.config._module.features))
then throw "no submodule defined"
else evaledSubmodule';
in {
options = {
module = mkOption {
description = "Module defining submodule";
type = types.unspecified;
};
modules = mkOption {
description = "List of modules defining submodule";
type = types.listOf types.unspecified;
default = [config.module];
};
features = mkOption {
description = "List of features exposed by submodule";
type = types.listOf types.str;
};
definition = mkOption {
description = "Submodule definition";
type = types.attrs;
};
exportAs = mkOption {
description = "Name under which to register exports";
type = types.nullOr types.str;
default = null;
};
};
config = {
definition = {
inherit (evaledSubmodule.config.submodule) name description version tags exports;
};
features = evaledSubmodule.config._module.features;
};
})
)
);
default = [];
};
submodules.instances = mkOption {
description = "Attribute set of submodule instances";
default = {};
type = types.attrsOf (types.submodule ({name, config, options, ...}: let
# submodule associated with
submodule = findSubmodule {
name = config.submodule;
version = config.version;
};
# definition of a submodule
submoduleDefinition = submodule.definition;
# submodule defaults
defaults = getDefaults {
name = submoduleDefinition.name;
version = submoduleDefinition.version;
tags = submoduleDefinition.tags;
features = submodule.features;
};
in {
options = {
name = mkOption {
description = "Submodule instance name";
type = types.str;
default = name;
};
submodule = mkOption {
description = "Name of the submodule to use";
type = types.str;
default = name;
};
version = mkOption {
description = ''
Version of submodule to use, if version starts with "~" it is
threated as regex pattern for example "~1.0.*"
'';
type = types.nullOr types.str;
default = null;
};
passthru.enable = mkOption {
description = "Whether to passthru submodule resources";
type = types.bool;
default = true;
};
config = mkOption {
description = "Submodule instance ${config.name} for ${submoduleDefinition.name}:${submoduleDefinition.version} config";
type = submoduleWithSpecialArgs ({...}: {
imports = submodule.modules ++ defaults ++ [ ./base.nix ];
_module.args.pkgs = pkgs;
_module.args.name = config.name;
_module.args.submodule = config;
submodule.args = mkAliasDefinitions options.args;
}) specialArgs;
default = {};
};
args = mkOption {
description = "Submodule arguments (alias of config.submodule.args)";
};
};
}));
};
default = {};
};
config = mkMerge ([
{
# register exported functions as args
_module.args = mkMerge (map (submodule: {
${submodule.exportAs} = submodule.definition.exports;
}) (filter (submodule: submodule.exportAs != null) cfg.imports));
_module.features = ["submodules"];
submodules.specialArgs.kubenix = kubenix;
# passthru kubenix.project to submodules
submodules.defaults = mkMerge [
[{
default = {
kubenix.project = parentConfig.kubenix.project;
};
}]
(map (propagate: {
features = propagate.features;
default = propagate.module;
}) config._module.propagate)
];
}
(mkIf cfg.propagate.enable {
# if propagate is enabled and submodule has submodules included propagage defaults and imports
submodules.defaults = [{
features = ["submodules"];
default = {
submodules = {
defaults = cfg.defaults;
imports = cfg.imports;
};
};
}];
})
] ++ passthruConfig);
}

67
modules/test.nix Normal file
View file

@ -0,0 +1,67 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.test;
in {
options.test = {
name = mkOption {
description = "Test name";
type = types.str;
};
description = mkOption {
description = "Test description";
type = types.str;
};
enable = mkOption {
description = "Whether to enable test";
type = types.bool;
default = true;
};
assertions = mkOption {
type = types.listOf (types.submodule {
options = {
assertion = mkOption {
description = "assertion value";
type = types.bool;
default = false;
};
message = mkOption {
description = "assertion message";
type = types.str;
};
};
});
default = [];
example = [ { assertion = false; message = "you can't enable this for that reason"; } ];
description = ''
This option allows modules to express conditions that must
hold for the evaluation of the system configuration to
succeed, along with associated error messages for the user.
'';
};
extraCheckInputs = mkOption {
description = "Extra check inputs";
type = types.listOf types.package;
default = [];
};
testScript = mkOption {
description = "Script to run as part of testing";
type = types.nullOr types.lines;
default = null;
};
extraConfiguration = mkOption {
description = "Extra configuration for running test";
type = types.unspecified;
default = {};
};
};
}

291
modules/testing.nix Normal file
View file

@ -0,0 +1,291 @@
{ nixosPath, config, pkgs, lib, kubenix, ... }:
with lib;
let
cfg = config.testing;
toJSONFile = content: builtins.toFile "json" (builtins.toJSON content);
nixosTesting = import "${nixosPath}/lib/testing.nix" {
inherit pkgs;
system = "x86_64-linux";
};
kubernetesBaseConfig = { modulesPath, config, pkgs, lib, nodes, ... }: let
master = findFirst
(node: any (role: role == "master") node.config.services.kubernetes.roles)
(throw "no master node")
(attrValues nodes);
extraHosts = ''
${master.config.networking.primaryIPAddress} etcd.${config.networking.domain}
${master.config.networking.primaryIPAddress} api.${config.networking.domain}
${concatMapStringsSep "\n"
(node: let n = node.config.networking; in "${n.primaryIPAddress} ${n.hostName}.${n.domain}")
(attrValues nodes)}
'';
in {
imports = [ "${toString modulesPath}/profiles/minimal.nix" ];
config = mkMerge [{
boot.postBootCommands = "rm -fr /var/lib/kubernetes/secrets /tmp/shared/*";
virtualisation.memorySize = mkDefault 2048;
virtualisation.cores = mkDefault 16;
virtualisation.diskSize = mkDefault 4096;
networking = {
inherit extraHosts;
domain = "my.xzy";
nameservers = ["10.0.0.254"];
firewall = {
allowedTCPPorts = [
10250 # kubelet
];
trustedInterfaces = ["docker0" "cni0"];
extraCommands = concatMapStrings (node: ''
iptables -A INPUT -s ${node.config.networking.primaryIPAddress} -j ACCEPT
'') (attrValues nodes);
};
};
environment.systemPackages = [ pkgs.kubectl ];
environment.variables.KUBECONFIG = "/etc/kubernetes/cluster-admin.kubeconfig";
services.flannel.iface = "eth1";
services.kubernetes = {
easyCerts = true;
apiserver = {
securePort = 443;
advertiseAddress = master.config.networking.primaryIPAddress;
};
masterAddress = "${master.config.networking.hostName}.${master.config.networking.domain}";
};
systemd.extraConfig = "DefaultLimitNOFILE=1048576";
}
(mkIf (any (role: role == "master") config.services.kubernetes.roles) {
networking.firewall.allowedTCPPorts = [
443 # kubernetes apiserver
];
})];
};
mkKubernetesSingleNodeTest = { name, testScript, extraConfiguration ? {} }:
nixosTesting.makeTest {
inherit name;
nodes.kube = { config, pkgs, nodes, ... }: {
imports = [ kubernetesBaseConfig extraConfiguration ];
services.kubernetes = {
roles = ["master" "node"];
flannel.enable = false;
kubelet = {
networkPlugin = "cni";
cni.config = [{
name = "mynet";
type = "bridge";
bridge = "cni0";
addIf = true;
ipMasq = true;
isGateway = true;
ipam = {
type = "host-local";
subnet = "10.1.0.0/16";
gateway = "10.1.0.1";
routes = [{
dst = "0.0.0.0/0";
}];
};
}];
};
};
networking.primaryIPAddress = mkForce "192.168.1.1";
};
testScript = ''
startAll;
$kube->waitUntilSucceeds("kubectl get node kube.my.xzy | grep -w Ready");
${testScript}
'';
};
testOptions = { config, ... }: let
modules = [config.module ./test.nix ./base.nix {
config = {
kubenix.project = mkDefault config.name;
_module.args = {
test = config;
} // cfg.args;
};
}] ++ cfg.defaults;
test = (kubenix.evalModules {
check = false;
inherit modules;
}).config.test;
evaled' = kubenix.evalModules {
inherit modules;
};
evaled =
if cfg.throwError then evaled'
else if (builtins.tryEval evaled'.config.test.assertions).success then evaled' else null;
in {
options = {
name = mkOption {
description = "test name";
type = types.str;
internal = true;
};
description = mkOption {
description = "test description";
type = types.str;
internal = true;
};
enable = mkOption {
description = "Whether to enable test";
type = types.bool;
internal = true;
};
module = mkOption {
description = "Module defining submodule";
type = types.unspecified;
};
evaled = mkOption {
description = "Test evaulation result";
type = types.nullOr types.attrs;
internal = true;
};
success = mkOption {
description = "Whether test was success";
type = types.bool;
internal = true;
default = false;
};
assertions = mkOption {
description = "Test result";
type = types.unspecified;
internal = true;
default = [];
};
test = mkOption {
description = "Test derivation to run";
type = types.nullOr types.package;
default = null;
};
result = mkOption {
description = "Test result";
type = types.package;
};
};
config = mkMerge [{
inherit evaled;
inherit (test) name description enable;
result = pkgs.runCommand "${cfg.name}-${config.name}-test.json" {
buildInputs = [ pkgs.jshon pkgs.jq ];
} ''
jshon -n object \
-s "${config.name}" -i name \
-s "${config.description}" -i description \
-n "${if config.success then "true" else "false"}" -i success \
${if config.test == null then "-n null" else "-s '${config.test}'"} -i test > result.json
jq -s '.[0].assertions = .[1] | .[0]' result.json ${toJSONFile (map (getAttrs ["assertion" "message"]) config.assertions)} > $out
'';
} (mkIf (config.evaled != null) {
inherit (evaled.config.test) assertions;
success = all (el: el.assertion) config.assertions;
test =
if cfg.e2e && evaled.config.test.testScript != null
then mkKubernetesSingleNodeTest {
name = config.name;
inherit (evaled.config.test) testScript extraConfiguration;
} else null;
})];
};
in {
options = {
testing.name = mkOption {
description = "Testing suite name";
type = types.str;
default = "default";
};
testing.throwError = mkOption {
description = "Whether to throw error";
type = types.bool;
default = true;
};
testing.e2e = mkOption {
description = "Whether to enable e2e tests";
type = types.bool;
default = true;
};
testing.defaults = mkOption {
description = "Testing defaults";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
example = literalExample ''{config, ...}: {
kubernetes.version = config.kubernetes.version;
}'';
apply = filter (v: v!=[]);
default = [];
};
testing.tests = mkOption {
description = "List of test cases";
default = [];
type = types.listOf (types.coercedTo types.path (module: {inherit module;}) (types.submodule testOptions));
apply = tests: filter (test: test.enable) tests;
};
testing.args = mkOption {
description = "Attribute set of extra args passed to tests";
type = types.attrs;
default = {};
};
testing.testsByName = mkOption {
description = "Tests by name";
type = types.attrsOf types.attrs;
default = listToAttrs (map (test: nameValuePair test.name test) cfg.tests);
};
testing.success = mkOption {
description = "Whether testing was a success";
type = types.bool;
default = all (test: test.success) cfg.tests;
};
testing.results = mkOption {
description = "Test results";
type = types.attrsOf types.package;
default = mapAttrs (_: t: t.result) cfg.testsByName;
};
testing.result = mkOption {
description = "Testing results";
type = types.package;
default = pkgs.runCommand "${cfg.name}-test-results.json" {
buildInputs = [ pkgs.jq ];
} ''
jq -s -r '.' ${concatMapStringsSep " " (t: t.result) cfg.tests} > tests.json
jq -n \
--rawfile tests tests.json \
--argjson success `jq -s -r 'if all(.success) == true then true else false end' ${concatMapStringsSep " " (t: t.result) cfg.tests}` \
'{"success": $success, "tests": $tests | fromjson }' > $out
'';
};
};
}

148
release.nix Normal file
View file

@ -0,0 +1,148 @@
{ pkgs ? import <nixpkgs> {}, nixosPath ? toString <nixpkgs/nixos>, lib ? pkgs.lib
, e2e ? true, throwError ? true }:
with lib;
let
kubenix = import ./. { inherit pkgs; };
lib = kubenix.lib;
generateK8S = name: spec: import ./generators/k8s {
inherit name;
inherit pkgs;
inherit (pkgs) lib;
inherit spec;
};
generateIstio = import ./generators/istio {
inherit pkgs;
inherit (pkgs) lib;
};
runK8STests = k8sVersion: import ./tests {
inherit pkgs lib kubenix k8sVersion e2e throwError nixosPath;
};
in rec {
generate.k8s = pkgs.linkFarm "k8s-generated.nix" [
{
name = "v1.7.nix";
path = generateK8S "v1.7" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.7.16";
sha256 = "1ksalw3hzbcca89n9h3pas9nqj2n5gq3rbpdx633ycqb8g46h1iw";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.8.nix";
path = generateK8S "v1.8" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.8.15";
sha256 = "1mwaafnkimr4kwqws4qli11wbavpmf27i6pjq77sfsapw9sz54j4";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.9.nix";
path = generateK8S "v1.9" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.9.11";
sha256 = "1wl944ci7k8knrkdrc328agyq4c953j9dm0sn314s42j18lfd7rv";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.10.nix";
path = generateK8S "v1.10" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.10.13";
sha256 = "07hwcamlc1kh5flwv4ahfkcg2lyhnbs8q2xczaws6v3sjxaycrrn";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.11.nix";
path = generateK8S "v1.11" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.11.8";
sha256 = "1q6x38zdycd4ai31gn666hg41bs4q32dyz2d07x76hj33fkzqs1f";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.12.nix";
path = generateK8S "v1.12" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.12.6";
sha256 = "0p9wh264xfm4c0inz99jclf603c414807vn19gfn62bfls3jcmgf";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.13.nix";
path = generateK8S "v1.13" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.13.4";
sha256 = "1q3dc416fr9nzy64pl7rydahygnird0vpk9yflssw7v9gx84m6x9";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.14.nix";
path = generateK8S "v1.14" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.14.10";
sha256 = "0lkajm0qfi0qgcqm465z9bi04f778pg3qwnnkxlq38p7ibvi5vn4";
}}/api/openapi-spec/swagger.json";
}
{
name = "v1.15.nix";
path = generateK8S "v1.15" "${pkgs.fetchFromGitHub {
owner = "kubernetes";
repo = "kubernetes";
rev = "v1.15.7";
sha256 = "1hp6231c1l1fx9s182ivy1s6cgqlk208dj95dbhajd3qq8fdabqc";
}}/api/openapi-spec/swagger.json";
}
];
generate.istio = pkgs.linkFarm "istio-generated.nix" [{
name = "latest.nix";
path = generateIstio;
}];
tests = {
k8s-1_7 = runK8STests "1.7";
k8s-1_8 = runK8STests "1.8";
k8s-1_9 = runK8STests "1.9";
k8s-1_10 = runK8STests "1.10";
k8s-1_11 = runK8STests "1.11";
k8s-1_12 = runK8STests "1.12";
k8s-1_13 = runK8STests "1.13";
k8s-1_14 = runK8STests "1.14";
k8s-1_15 = runK8STests "1.15";
};
test-results = pkgs.recurseIntoAttrs (mapAttrs (_: t: pkgs.recurseIntoAttrs {
results = pkgs.recurseIntoAttrs t.results;
result = t.result;
}) tests);
test-check =
if !(all (test: test.success) (attrValues tests))
then throw "tests failed"
else true;
examples = import ./examples {};
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,8 +0,0 @@
{
"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"
}

View file

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

View file

@ -1,20 +0,0 @@
{
"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"
]
}
}
}

View file

@ -1,67 +0,0 @@
{
"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"
}
]
}
}
}
}

View file

@ -1,5 +0,0 @@
{ config, ... }:
{
require = [./modules.nix ./deployment.nix];
}

View file

@ -1,38 +0,0 @@
{
"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"
}
}
}
}
}
}
}
}

View file

@ -1,24 +0,0 @@
{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;
};
}

View file

@ -1,110 +0,0 @@
{lib, k8s, config, ...}:
with 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;
};
password = mkSecretOption {
description = "Nginx simple auth credentials";
default = null;
};
};
config = {
kubernetes.resources.deployments.nginx = mkMerge [
(loadJSON ./deployment.json)
{
metadata.name = "${name}-nginx";
spec.template.spec.containers.nginx.ports."80" = {
containerPort = config.port;
};
spec.template.spec.containers.nginx.env.name =
mkIf (config.password != null) (secretToEnv config.password);
}
];
kubernetes.resources.configMaps.nginx = mkMerge [
(loadJSON ./configMap.json)
{
metadata.name = mkForce "${name}-nginx";
}
];
};
};
kubernetes.modules.app-v1 = {
module = "nginx";
configuration.password.name = "test2";
configuration.password.key = "password";
configuration.kubernetes.resources.customResourceDefinitions.secret-claims = {
kind = "CustomResourceDefinition";
apiVersion = "apiextensions.k8s.io/v1beta1";
metadata.name = "secretclaims.vaultproject.io";
spec = {
group = "vaultproject.io";
version = "v1";
scope = "Namespaced";
names = {
plural = "secretclaims";
kind = "SecretClaim";
shortNames = ["scl"];
};
};
};
configuration.kubernetes.customResources.secret-claims.claim = {
metadata.name = "test";
};
};
kubernetes.modules.app-v2 = {
module = "nginx";
configuration.port = 8080;
configuration.kubernetes.modules.subsubmodule = {
module = "nginx";
configuration.kubernetes.resources.customResourceDefinitions.secret-claims = {
kind = "CustomResourceDefinition";
apiVersion = "apiextensions.k8s.io/v1beta1";
metadata.name = "secretclaims.vaultproject.io";
spec = {
group = "vaultproject.io";
version = "v1";
scope = "Namespaced";
names = {
plural = "secretclaims";
kind = "SecretClaim";
shortNames = ["scl"];
};
};
};
configuration.kubernetes.customResources.secret-claims.claim = {
metadata.name = "test";
};
};
};
kubernetes.resources.services.nginx = loadJSON ./service.json;
kubernetes.defaultModuleConfiguration.all = [{
config.kubernetes.defaults.deployments.spec.replicas = mkDefault 3;
}];
kubernetes.defaultModuleConfiguration.nginx = {config, name, ...}: {
kubernetes.defaults.deployments.spec.replicas = 4;
};
};
}

View file

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

View file

@ -1,26 +0,0 @@
{
"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
}
]
}
}

56
tests/default.nix Normal file
View file

@ -0,0 +1,56 @@
{ pkgs ? import <nixpkgs> {}
, lib ? pkgs.lib
, kubenix ? import ../. { inherit pkgs lib; }
, k8sVersion ? "1.13"
, nixosPath ? toString <nixpkgs/nixos>
# whether any testing error should throw an error
, throwError ? true
, e2e ? true }:
with lib;
let
images = pkgs.callPackage ./images.nix {};
test = (kubenix.evalModules {
modules = [
kubenix.modules.testing
{
testing.name = "k8s-${k8sVersion}";
testing.throwError = throwError;
testing.e2e = e2e;
testing.tests = [
./k8s/simple.nix
./k8s/deployment.nix
./k8s/crd.nix
./k8s/1.13/crd.nix
./k8s/defaults.nix
./k8s/order.nix
./k8s/submodule.nix
./k8s/imports.nix
./legacy/k8s.nix
./legacy/crd.nix
./legacy/modules.nix
./helm/simple.nix
./istio/bookinfo.nix
./submodules/simple.nix
./submodules/defaults.nix
./submodules/versioning.nix
./submodules/exports.nix
./submodules/passthru.nix
];
testing.args = {
inherit images k8sVersion;
};
}
];
args = {
inherit pkgs;
};
specialArgs = {
inherit kubenix nixosPath;
};
}).config;
in pkgs.recurseIntoAttrs test.testing

98
tests/helm/simple.nix Normal file
View file

@ -0,0 +1,98 @@
{ config, lib, pkgs, kubenix, helm, k8sVersion, ... }:
with lib;
with kubenix.lib;
with pkgs.dockerTools;
let
corev1 = config.kubernetes.api.resources.core.v1;
appsv1beta2 = config.kubernetes.api.resources.apps.v1beta2;
postgresql = pullImage {
imageName = "docker.io/bitnami/postgresql";
imageDigest = "sha256:16485a9b19696958ab7259e0d2c3efa0ef7f300b6fd1beb13a6643e120970a05";
sha256 = "0hqdkpk7s3wcy5qsy6dzgzqc0rbpavpghly350p97j1janxbyhc7";
finalImageTag = "10.7.0";
};
postgresqlExporter = pullImage {
imageName = "docker.io/wrouesnel/postgres_exporter";
imageDigest = "sha256:dd8051322ceb8995d3d7f116041a2116815e01e88232a90f635ebde8dcc4d3f4";
sha256 = "09mva5jx1g4v47s4lr1pkpfzzmxc7z9dnajfizffm3rxwl0qzjji";
finalImageTag = "v0.4.7";
};
minideb = pullImage {
imageName = "docker.io/bitnami/minideb";
imageDigest = "sha256:363011b4ad5308e7f2aee505b80730cbaadf9d41ff87879403f567dd98cfb5cf";
sha256 = "1vfyfdhmgidi7hc8kjflpq91vkzdqi9sj78g51ci8nyarclr808q";
finalImageTag = "latest";
};
in {
imports = [ kubenix.modules.test kubenix.modules.helm kubenix.modules.k8s ];
test = {
name = "helm-simple";
description = "Simple k8s testing wheter name, apiVersion and kind are preset";
enable = builtins.compareVersions config.kubernetes.version "1.8" >= 0;
assertions = [{
message = "should have generated resources";
assertion =
appsv1beta2.StatefulSet ? "app-psql-postgresql-master" &&
appsv1beta2.StatefulSet ? "app-psql-postgresql-slave" &&
corev1.ConfigMap ? "app-psql-postgresql-init-scripts" &&
corev1.Secret ? "app-psql-postgresql" &&
corev1.Service ? "app-psql-postgresql-headless" ;
} {
message = "should have values passed";
assertion = appsv1beta2.StatefulSet.app-psql-postgresql-slave.spec.replicas == 2;
} {
message = "should have namespace defined";
assertion =
appsv1beta2.StatefulSet.app-psql-postgresql-master.metadata.namespace == "test";
}];
testScript = ''
$kube->waitUntilSucceeds("docker load < ${postgresql}");
$kube->waitUntilSucceeds("docker load < ${postgresqlExporter}");
$kube->waitUntilSucceeds("docker load < ${minideb}");
$kube->waitUntilSucceeds("kubectl apply -f ${config.kubernetes.result}");
$kube->waitUntilSucceeds("PGPASSWORD=postgres ${pkgs.postgresql}/bin/psql -h app-psql-postgresql.test.svc.cluster.local -U postgres -l");
'';
};
kubernetes.version = k8sVersion;
kubernetes.resources.namespaces.test = {};
kubernetes.helm.instances.app-psql = {
namespace = "test";
chart = helm.fetch {
repo = "https://kubernetes-charts.storage.googleapis.com/";
chart = "postgresql";
version = "3.0.0";
sha256 = "06dkn4fgvgqr27hcnbbax1ylvr4sld3rcmy1w5kanljsajbph57m";
};
values = {
image = {
repository = "bitnami/postgresql";
tag = "10.7.0";
pullPolicy = "IfNotPresent";
};
volumePermissions.image = {
repository = "bitnami/minideb";
tag = "latest";
pullPolicy = "IfNotPresent";
};
metrics.image = {
repository = "wrouesnel/postgres_exporter";
tag = "v0.4.7";
pullPolicy = "IfNotPresent";
};
replication.enabled = true;
replication.slaveReplicas = 2;
postgresqlPassword = "postgres";
persistence.enabled = false;
};
};
}

45
tests/images.nix Normal file
View file

@ -0,0 +1,45 @@
{ pkgs, dockerTools, lib, ... }:
with lib;
{
nginx = let
nginxPort = "80";
nginxConf = pkgs.writeText "nginx.conf" ''
user nginx nginx;
daemon off;
error_log /dev/stdout info;
pid /dev/null;
events {}
http {
access_log /dev/stdout;
server {
listen ${nginxPort};
index index.html;
location / {
root ${nginxWebRoot};
}
}
}
'';
nginxWebRoot = pkgs.writeTextDir "index.html" ''
<html><body><h1>Hello from NGINX</h1></body></html>
'';
in dockerTools.buildLayeredImage {
name = "xtruder/nginx";
tag = "latest";
contents = [pkgs.nginx];
extraCommands = ''
mkdir -p etc
chmod u+w etc
echo "nginx:x:1000:1000::/:" > etc/passwd
echo "nginx:x:1000:nginx" > etc/group
'';
config = {
Cmd = ["nginx" "-c" nginxConf];
ExposedPorts = {
"${nginxPort}/tcp" = {};
};
};
};
}

110
tests/istio/bookinfo.nix Normal file
View file

@ -0,0 +1,110 @@
{ config, kubenix, k8sVersion, ... }:
{
imports = with kubenix.modules; [ test k8s istio ];
test = {
name = "istio-bookinfo";
description = "Simple istio bookinfo application (WIP)";
};
kubernetes.version = k8sVersion;
kubernetes.api."networking.istio.io"."v1alpha3" = {
Gateway."bookinfo-gateway" = {
spec = {
selector.istio = "ingressgateway";
servers = [{
port = {
number = 80;
name = "http";
protocol = "HTTP";
};
hosts = ["*"];
}];
};
};
VirtualService.bookinfo = {
spec = {
hosts = ["*"];
gateways = ["bookinfo-gateway"];
http = [{
match = [{
uri.exact = "/productpage";
} {
uri.exact = "/login";
} {
uri.exact = "/logout";
} {
uri.prefix = "/api/v1/products";
}];
route = [{
destination = {
host = "productpage";
port.number = 9080;
};
}];
}];
};
};
DestinationRule.productpage = {
spec = {
host = "productpage";
subsets = [{
name = "v1";
labels.version = "v1";
}];
};
};
DestinationRule.reviews = {
spec = {
host = "reviews";
subsets = [{
name = "v1";
labels.version = "v1";
} {
name = "v2";
labels.version = "v2";
} {
name = "v3";
labels.version = "v3";
}];
};
};
DestinationRule.ratings = {
spec = {
host = "ratings";
subsets = [{
name = "v1";
labels.version = "v1";
} {
name = "v2";
labels.version = "v2";
} {
name = "v2-mysql";
labels.version = "v2-mysql";
} {
name = "v2-mysql-vm";
labels.version = "v2-mysql-vm";
}];
};
};
DestinationRule.details = {
spec = {
host = "details";
subsets = [{
name = "v1";
labels.version = "v1";
} {
name = "v2";
labels.version = "v2";
}];
};
};
};
}

40
tests/k8s/1.13/crd.nix Normal file
View file

@ -0,0 +1,40 @@
{ config, lib, kubenix, k8sVersion, ... }:
with lib;
let
cfg = config.kubernetes.resources.customResourceDefinitions.crontabs;
in {
imports = with kubenix.modules; [ test k8s ];
test = {
name = "k8s-1-13-crd";
description = "Simple test testing CRD for k8s 1.13";
enable = builtins.compareVersions config.kubernetes.version "1.13" >= 0;
assertions = [{
message = "should have versions set";
assertion = (head cfg.spec.versions).name == "v1";
}];
};
kubernetes.version = k8sVersion;
kubernetes.resources.customResourceDefinitions.crontabs = {
metadata.name = "crontabs.stable.example.com";
spec = {
group = "stable.example.com";
versions = [{
name = "v1";
served = true;
storage = true;
}];
scope = "Namespaced";
names = {
plural = "crontabs";
singular = "crontab";
kind = "CronTab";
shortNames = ["ct"];
};
};
};
}

124
tests/k8s/crd.nix Normal file
View file

@ -0,0 +1,124 @@
{ config, lib, kubenix, pkgs, k8sVersion, ... }:
with lib;
let
crd = config.kubernetes.api.resources.customResourceDefinitions.crontabs;
latestCrontab = config.kubernetes.api.resources.cronTabs.latest;
in {
imports = with kubenix.modules; [ test k8s ];
test = {
name = "k8s-crd";
description = "Simple test tesing CRD";
enable = builtins.compareVersions config.kubernetes.version "1.8" >= 0;
assertions = [{
message = "CRD should have group and version set";
assertion =
crd.spec.group == "stable.example.com" &&
crd.spec.version == "v1";
} {
message = "Custom resource should have correct version set";
assertion = latestCrontab.apiVersion == "stable.example.com/v2";
}];
testScript = ''
$kube->waitUntilSucceeds("kubectl apply -f ${config.kubernetes.result}");
$kube->succeed("kubectl get crds | grep -i crontabs");
$kube->succeed("kubectl get crontabs | grep -i versioned");
$kube->succeed("kubectl get crontabs | grep -i latest");
'';
};
kubernetes.version = k8sVersion;
kubernetes.resources.customResourceDefinitions.crontabs = {
metadata.name = "crontabs.stable.example.com";
spec = {
group = "stable.example.com";
version = "v1";
scope = "Namespaced";
names = {
plural = "crontabs";
singular = "crontab";
kind = "CronTab";
shortNames = ["ct"];
};
};
};
kubernetes.resources.customResourceDefinitions.crontabsv2 = {
metadata.name = "crontabs.stable.example.com";
spec = {
group = "stable.example.com";
version = "v2";
scope = "Namespaced";
names = {
plural = "crontabs";
singular = "crontab";
kind = "CronTab";
shortNames = ["ct"];
};
};
};
kubernetes.createCustomTypesFromCRDs = true;
kubernetes.customTypes = mkMerge [
{
"stable.example.com/v1/CronTab" = {
attrName = "cronTabs";
description = "CronTabs resources";
module = {
options.schedule = mkOption {
description = "Crontab schedule script";
type = types.str;
};
};
};
}
{
"stable.example.com/v2/CronTab" = {
description = "CronTabs resources";
attrName = "cronTabs";
module = {
options = {
schedule = mkOption {
description = "Crontab schedule script";
type = types.str;
};
command = mkOption {
description = "Command to run";
type = types.str;
};
};
};
};
}
[{
group = "stable.example.com";
version = "v3";
kind = "CronTab";
description = "CronTabs resources";
attrName = "cronTabsV3";
module = {
options = {
schedule = mkOption {
description = "Crontab schedule script";
type = types.str;
};
command = mkOption {
description = "Command to run";
type = types.str;
};
};
};
}]
];
kubernetes.resources."stable.example.com"."v1".CronTab.versioned.spec.schedule = "* * * * *";
kubernetes.resources.cronTabs.latest.spec.schedule = "* * * * *";
kubernetes.resources.cronTabsV3.latest.spec.schedule = "* * * * *";
}

50
tests/k8s/defaults.nix Normal file
View file

@ -0,0 +1,50 @@
{ config, lib, kubenix, k8sVersion, ... }:
with lib;
let
pod1 = config.kubernetes.api.resources.pods.pod1;
pod2 = config.kubernetes.api.resources.pods.pod2;
in {
imports = with kubenix.modules; [ test k8s ];
test = {
name = "k8s-defaults";
description = "Simple k8s testing wheter name, apiVersion and kind are preset";
assertions = [{
message = "Should have label set with resource";
assertion = pod1.metadata.labels.resource-label == "value";
} {
message = "Should have default label set with group, version, kind";
assertion = pod1.metadata.labels.gvk-label == "value";
} {
message = "Should have conditional annotation set";
assertion = pod2.metadata.annotations.conditional-annotation == "value";
}];
};
kubernetes.version = k8sVersion;
kubernetes.resources.pods.pod1 = {};
kubernetes.resources.pods.pod2 = {
metadata.labels.custom-label = "value";
};
kubernetes.api.defaults = [{
resource = "pods";
default.metadata.labels.resource-label = "value";
} {
group = "core";
kind = "Pod";
version = "v1";
default.metadata.labels.gvk-label = "value";
} {
resource = "pods";
default = { config, ... }: {
config.metadata.annotations = mkIf (config.metadata.labels ? "custom-label") {
conditional-annotation = "value";
};
};
}];
}

69
tests/k8s/deployment.nix Normal file
View file

@ -0,0 +1,69 @@
{ config, lib, pkgs, kubenix, images, k8sVersion, ... }:
with lib;
let
cfg = config.kubernetes.api.resources.deployments.nginx;
image = images.nginx;
in {
imports = [ kubenix.modules.test kubenix.modules.k8s kubenix.modules.docker ];
test = {
name = "k8s-deployment";
description = "Simple k8s testing a simple deployment";
assertions = [{
message = "should have correct apiVersion and kind set";
assertion =
if ((builtins.compareVersions config.kubernetes.version "1.7") <= 0)
then cfg.apiVersion == "apps/v1beta1"
else if ((builtins.compareVersions config.kubernetes.version "1.8") <= 0)
then cfg.apiVersion == "apps/v1beta2"
else cfg.apiVersion == "apps/v1";
} {
message = "should have corrent kind set";
assertion = cfg.kind == "Deployment";
} {
message = "should have replicas set";
assertion = cfg.spec.replicas == 10;
}];
extraConfiguration = {
environment.systemPackages = [ pkgs.curl ];
services.kubernetes.kubelet.seedDockerImages = config.docker.export;
};
testScript = ''
$kube->waitUntilSucceeds("kubectl apply -f ${config.kubernetes.result}");
$kube->succeed("kubectl get deployment | grep -i nginx");
$kube->waitUntilSucceeds("kubectl get deployment -o go-template nginx --template={{.status.readyReplicas}} | grep 10");
$kube->waitUntilSucceeds("curl http://nginx.default.svc.cluster.local | grep -i hello");
'';
};
docker.images.nginx.image = image;
kubernetes.version = k8sVersion;
kubernetes.resources.deployments.nginx = {
spec = {
replicas = 10;
selector.matchLabels.app = "nginx";
template.metadata.labels.app = "nginx";
template.spec = {
containers.nginx = {
image = config.docker.images.nginx.path;
imagePullPolicy = "Never";
};
};
};
};
kubernetes.resources.services.nginx = {
spec = {
ports = [{
name = "http";
port = 80;
}];
selector.app = "nginx";
};
};
}

21
tests/k8s/deployment.yaml Normal file
View file

@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

30
tests/k8s/imports.nix Normal file
View file

@ -0,0 +1,30 @@
{ config, lib, kubenix, k8sVersion, ... }:
with lib;
let
pod = config.kubernetes.api.resources.core.v1.Pod.test;
deployment = config.kubernetes.api.resources.apps.v1.Deployment.nginx-deployment;
in {
imports = with kubenix.modules; [ test k8s ];
test = {
name = "k8s-imports";
description = "Simple k8s testing imports";
enable = builtins.compareVersions config.kubernetes.version "1.10" >= 0;
assertions = [{
message = "Pod should have name set";
assertion = pod.metadata.name == "test";
} {
message = "Deployment should have name set";
assertion = deployment.metadata.name == "nginx-deployment";
}];
};
kubernetes.version = k8sVersion;
kubernetes.imports = [
./pod.json
./deployment.yaml
];
}

59
tests/k8s/order.nix Normal file
View file

@ -0,0 +1,59 @@
{ config, lib, kubenix, pkgs, k8sVersion, ... }:
with lib;
let
cfg = config.kubernetes.api.resources.customResourceDefinitions.crontabs;
in {
imports = with kubenix.modules; [ test k8s ];
test = {
name = "k8s-order";
description = "test tesing k8s resource order";
enable = builtins.compareVersions config.kubernetes.version "1.8" >= 0;
assertions = [{
message = "should have correct order of resources";
assertion =
(elemAt config.kubernetes.objects 0).kind == "CustomResourceDefinition" &&
(elemAt config.kubernetes.objects 1).kind == "Namespace" &&
(elemAt config.kubernetes.objects 2).kind == "CronTab";
}];
};
kubernetes.version = k8sVersion;
kubernetes.resources.customResourceDefinitions.crontabs = {
metadata.name = "crontabs.stable.example.com";
spec = {
group = "stable.example.com";
version = "v1";
scope = "Namespaced";
names = {
plural = "crontabs";
singular = "crontab";
kind = "CronTab";
shortNames = ["ct"];
};
};
};
kubernetes.customTypes = [{
name = "crontabs";
description = "CronTabs resources";
attrName = "cronTabs";
group = "stable.example.com";
version = "v1";
kind = "CronTab";
module = {
options.schedule = mkOption {
description = "Crontab schedule script";
type = types.str;
};
};
}];
kubernetes.resources.namespaces.test = {};
kubernetes.resources."stable.example.com"."v1".CronTab.crontab.spec.schedule = "* * * * *";
}

13
tests/k8s/pod.json Normal file
View file

@ -0,0 +1,13 @@
{
"kind": "Pod",
"apiVersion": "v1",
"metadata": {
"name": "test"
},
"spec": {
"containers": [{
"name": "test",
"image": "busybox"
}]
}
}

23
tests/k8s/simple.nix Normal file
View file

@ -0,0 +1,23 @@
{ config, kubenix, k8sVersion, ... }:
let
cfg = config.kubernetes.api.resources.pods.nginx;
in {
imports = [ kubenix.modules.test kubenix.modules.k8s ];
test = {
name = "k8s-simple";
description = "Simple k8s testing wheter name, apiVersion and kind are preset";
assertions = [{
message = "should have apiVersion and kind set";
assertion = cfg.apiVersion == "v1" && cfg.kind == "Pod";
} {
message = "should have name set";
assertion = cfg.metadata.name == "nginx";
}];
};
kubernetes.version = k8sVersion;
kubernetes.resources.pods.nginx = {};
}

60
tests/k8s/submodule.nix Normal file
View file

@ -0,0 +1,60 @@
{ name, config, lib, kubenix, images, ... }:
with lib;
let
cfg = config.submodules.instances.passthru;
in {
imports = with kubenix.modules; [ test submodules k8s docker ];
test = {
name = "k8s-submodule";
description = "Simple k8s submodule test";
assertions = [{
message = "Submodule has correct name set";
assertion = (head config.kubernetes.objects).metadata.name == "passthru";
} {
message = "Should expose docker image";
assertion = (head config.docker.export).imageName == "xtruder/nginx";
}];
};
kubernetes.namespace = "test-namespace";
submodules.imports = [{
module = {name, config, ...}: {
imports = with kubenix.modules; [ submodule k8s docker ];
config = {
submodule = {
name = "test-submodule";
passthru = {
kubernetes.objects = config.kubernetes.objects;
docker.images = config.docker.images;
};
};
kubernetes.resources.pods.nginx = {
metadata.name = name;
spec.containers.nginx.image = config.docker.images.nginx.path;
};
docker.images.nginx.image = images.nginx;
};
};
}];
kubernetes.api.defaults = [{
propagate = true;
default.metadata.labels.my-label = "my-value";
}];
submodules.instances.passthru = {
submodule = "test-submodule";
};
submodules.instances.no-passthru = {
submodule = "test-submodule";
passthru.enable = false;
};
}

106
tests/legacy/crd.nix Normal file
View file

@ -0,0 +1,106 @@
{ options, config, lib, kubenix, pkgs, k8sVersion, ... }:
with lib;
let
findObject = { kind, name }: filter (object:
object.kind == kind && object.metadata.name == name
) config.kubernetes.objects;
getObject = filter: head (findObject filter);
hasObject = { kind, name }: length (findObject { inherit kind name; }) == 1;
in {
imports = with kubenix.modules; [ test k8s legacy ];
test = {
name = "legacy-crd";
description = "Simple test tesing kubenix legacy integration with crds crd";
enable = builtins.compareVersions config.kubernetes.version "1.13" >= 0;
assertions = [{
message = "should define crd in module";
assertion =
hasObject {kind = "SecretClaim"; name = "module-claim";};
} {
message = "should define crd in root";
assertion =
hasObject {kind = "SecretClaim"; name = "root-claim";};
}];
};
kubernetes.version = k8sVersion;
kubernetes.namespace = "test";
kubernetes.moduleDefinitions.secret-claim.module = { config, k8s, module, ... }: {
options = {
name = mkOption {
description = "Name of the secret claim";
type = types.str;
default = module.name;
};
type = mkOption {
description = "Type of the secret";
type = types.enum ["Opaque" "kubernetes.io/tls"];
default = "Opaque";
};
path = mkOption {
description = "Secret path";
type = types.str;
};
renew = mkOption {
description = "Renew time in seconds";
type = types.nullOr types.int;
default = null;
};
data = mkOption {
type = types.nullOr types.attrs;
description = "Data to pass to get secrets";
default = null;
};
};
config = {
kubernetes.resources.customResourceDefinitions.secret-claims = {
kind = "CustomResourceDefinition";
apiVersion = "apiextensions.k8s.io/v1beta1";
metadata.name = "secretclaims.vaultproject.io";
spec = {
group = "vaultproject.io";
version = "v1";
scope = "Namespaced";
names = {
plural = "secretclaims";
kind = "SecretClaim";
shortNames = ["scl"];
};
};
};
kubernetes.customResources.secret-claims.claim = {
metadata.name = config.name;
spec = {
inherit (config) type path;
} // (optionalAttrs (config.renew != null) {
inherit (config) renew;
}) // (optionalAttrs (config.data != null) {
inherit (config) data;
});
};
};
};
kubernetes.modules.module-claim = {
module = "secret-claim";
configuration.path = "tokens/test";
};
kubernetes.customResources.secret-claims.root-claim = {
spec = {
path = "secrets/test2";
};
};
}

57
tests/legacy/k8s.nix Normal file
View file

@ -0,0 +1,57 @@
{ config, lib, kubenix, pkgs, k8sVersion, ... }:
with lib;
let
cfg = config.kubernetes.api.resources.deployments.app;
in {
imports = with kubenix.modules; [ test k8s legacy ];
test = {
name = "legacy-k8s";
description = "Simple test kubenix legacy kubernetes support";
assertions = [{
message = "should have correct resource options set";
assertion =
cfg.kind == "Deployment" &&
cfg.metadata.name == "app";
} {
message = "should have correct defaults set";
assertion =
cfg.metadata.namespace == "test" &&
cfg.metadata.labels.label1 == "value1" &&
cfg.metadata.labels.label2 == "value2";
}];
};
kubernetes.version = k8sVersion;
kubernetes.resources.deployments.app = {
spec = {
replicas = 2;
selector = {
matchLabels.app = "app";
};
template.spec = {
containers.app = {
image = "hello-world";
};
};
};
};
kubernetes.resources.configMaps.app = {
data."my-conf.json" = builtins.toJSON {};
};
kubernetes.defaults = {
all = [{
metadata.namespace = "test";
metadata.labels.label1 = "value1";
}];
deployments = [{
metadata.labels.label2 = "value2";
}];
};
}

108
tests/legacy/modules.nix Normal file
View file

@ -0,0 +1,108 @@
{ options, config, lib, kubenix, pkgs, k8sVersion, ... }:
with lib;
let
findObject = { kind, name }: filter (object:
object.kind == kind && object.metadata.name == name
) config.kubernetes.objects;
getObject = filter: head (findObject filter);
hasObject = { kind, name }: length (findObject { inherit kind name; }) == 1;
in {
imports = with kubenix.modules; [ test k8s legacy ];
test = {
name = "legacy-modules";
description = "Simple test tesing kubenix legacy modules";
assertions = [{
message = "should have all objects";
assertion =
hasObject {kind = "Deployment"; name = "myapp";} &&
hasObject {kind = "Deployment"; name = "myapp2";} &&
hasObject {kind = "Deployment"; name = "myapp2-app2";};
} {
message = "should have default labels set";
assertion =
(getObject {kind = "Deployment"; name = "myapp2-app2";})
.metadata.labels.module-label or false == "value" &&
(getObject {kind = "Deployment"; name = "myapp2";})
.metadata.labels.module-label or false == "value";
} {
message = "should passthru resources to root module";
assertion =
config.kubernetes.resources.deployments.myapp2-app2-app.metadata.labels.module-label or false == "value";
}];
};
kubernetes.version = k8sVersion;
kubernetes.defaults.all.metadata.labels.module-label = "value";
# propagate default module configuration and defaults
kubernetes.defaultModuleConfiguration = {
all.kubernetes.defaultModuleConfiguration = mkAliasDefinitions options.kubernetes.defaultModuleConfiguration;
all.kubernetes.defaults = mkAliasDefinitions options.kubernetes.defaults;
};
kubernetes.moduleDefinitions.app1.module = { config, k8s, module, ... }: {
config.kubernetes.resources.deployments.app = {
metadata.name = module.name;
spec = {
selector = {
matchLabels.app = "app";
};
template.spec = {
containers.app = {
image = "hello-world";
};
};
};
};
};
kubernetes.moduleDefinitions.app2.module = { name, config, k8s, module, ... }: {
options = {
replicas = mkOption {
description = "Number of replicas to run";
type = types.int;
default = 2;
};
};
config = {
kubernetes.resources.deployments.app = {
metadata.name = module.name;
spec = {
replicas = config.replicas;
selector = {
matchLabels.app = "app";
};
template.spec = {
containers.app = {
image = "hello-world";
};
};
};
};
kubernetes.modules.app2 = {
name = "${name}-app2";
module = "app1";
namespace = module.namespace;
};
};
};
kubernetes.modules.myapp = {
module = "app1";
namespace = "test";
};
kubernetes.modules.myapp2 = {
module = "app2";
namespace = "test";
configuration.replicas = 3;
};
}

View file

@ -0,0 +1,25 @@
{ config, kubenix, ... }:
{
imports = [ kubenix.modules.test kubenix.modules.metacontroller ];
test = {
name = "metacontroller-controllers";
description = "Testing metacontroller custom resources";
};
kubernetes.api.compositecontrollers.test = {
spec = {
generateSelector = true;
parentResource = {
apiVersion = "ctl.enisoc.com/v1";
resource = "things";
};
childResources = [{
apiVersion = "v1";
resource = "pods";
}];
hooks.sync.webhook.url = "http://thing-controller.metacontroller/sync";
};
};
}

64
tests/module.nix Normal file
View file

@ -0,0 +1,64 @@
{ name, config, lib, kubenix, images, ... }:
with lib;
let
cfg = config.submodules.instances.test.config;
deployment = cfg.kubernetes.api.deployments.nginx;
in {
imports = [ kubenix.modules.test kubenix.module ];
test = {
name = "module";
description = "Test testing kubenix module";
assertions = [{
message = "Namespace not propagated";
assertion = deployment.metadata.namespace == "test";
} {
message = "Version not propagated";
assertion = cfg.kubernetes.version == config.kubernetes.version;
} {
message = "docker image should be added to exported images";
assertion = (head config.docker.export) == images.nginx;
}];
testScript = ''
$kube->waitUntilSucceeds("docker load < ${images.nginx}");
$kube->waitUntilSucceeds("kubectl apply -f ${config.kubernetes.result}");
$kube->succeed("kubectl get deployment -n test | grep -i test-nginx");
$kube->waitUntilSucceeds("kubectl get deployment -n test -o go-template test-nginx --template={{.status.readyReplicas}} | grep 1");
'';
};
submodules.imports = [{
module = {name, config, ...}: {
submodule.name = "nginx";
kubernetes.api.deployments.nginx = {
metadata = {
name = "${name}-nginx";
labels.app = name;
};
spec = {
replicas = 1;
selector.matchLabels.app = "nginx";
template.metadata.labels.app = "nginx";
template.spec = {
containers.nginx = {
image = config.docker.images.nginx.path;
imagePullPolicy = "Never";
};
};
};
};
docker.images.nginx.image = images.nginx;
};
}];
kubernetes.api.namespaces.test = {};
submodules.instances.test = {
submodule = "nginx";
namespace = "test";
};
}

View file

@ -0,0 +1,139 @@
{ name, config, lib, kubenix, ... }:
with lib;
let
instance1 = config.submodules.instances.instance1;
instance2 = config.submodules.instances.instance2;
instance3 = config.submodules.instances.instance3;
instance4 = config.submodules.instances.instance4;
instance5 = config.submodules.instances.instance5;
versioned-submodule = config.submodules.instances.versioned-submodule;
submodule = {name, ...}: {
imports = [ kubenix.modules.submodule ];
options.submodule.args = {
value = mkOption {
description = "Submodule value";
type = types.str;
};
defaultValue = mkOption {
description = "Submodule default value";
type = types.str;
};
};
};
in {
imports = with kubenix.modules; [ test submodules ];
test = {
name = "submodules-defaults";
description = "Simple submodule test";
assertions = [{
message = "should apply defaults by tag1";
assertion = instance1.config.submodule.args.value == "value1";
} {
message = "should apply defaults by tag2";
assertion = instance2.config.submodule.args.value == "value2";
} {
message = "should apply defaults by tag2";
assertion = instance3.config.submodule.args.value == "value2";
} {
message = "should apply defaults to all";
assertion =
instance1.config.submodule.args.defaultValue == "value" &&
instance2.config.submodule.args.defaultValue == "value";
} {
message = "instance1 and instance3 should have value of default-value";
assertion = instance3.config.submodule.args.defaultValue == "default-value";
} {
message = "should apply defaults by submodule name";
assertion = instance4.config.submodule.args.value == "value4";
} {
message = "should apply defaults by custom condition";
assertion = instance5.config.submodule.args.defaultValue == "my-custom-value";
} {
message = "should apply defaults to versioned submodule";
assertion = versioned-submodule.config.submodule.args.defaultValue == "versioned-submodule";
}];
};
submodules.imports = [{
modules = [submodule {
submodule = {
name = "submodule1";
tags = ["tag1"];
};
}];
} {
modules = [submodule {
submodule = {
name = "submodule2";
tags = ["tag2"];
};
}];
} {
modules = [submodule {
submodule = {
name = "submodule3";
tags = ["tag2"];
};
}];
} {
modules = [submodule {
submodule = {
name = "submodule4";
};
}];
} {
modules = [submodule {
submodule = {
name = "submodule5";
};
submodule.args.value = "custom-value";
}];
} {
modules = [submodule {
submodule = {
name = "versioned-submodule";
version = "2.0.0";
};
}];
}];
submodules.defaults = [{
default.submodule.args.defaultValue = mkDefault "value";
} {
tags = ["tag1"];
default.submodule.args.value = mkDefault "value1";
} {
tags = ["tag2"];
default.submodule.args.value = mkDefault "value2";
} {
name = "submodule4";
default.submodule.args.value = mkDefault "value4";
} {
default = {config, ...}: {
submodule.args.defaultValue = mkIf (config.submodule.args.value == "custom-value") "my-custom-value";
};
} {
name = "versioned-submodule";
version = "2.0.0";
default.submodule.args.value = mkDefault "versioned";
}];
submodules.instances.instance1.submodule = "submodule1";
submodules.instances.instance2.submodule = "submodule2";
submodules.instances.instance3 = {
submodule = "submodule3";
args.defaultValue = "default-value";
};
submodules.instances.instance4.submodule = "submodule4";
submodules.instances.instance5.submodule = "submodule5";
submodules.instances.versioned-submodule = {
submodule = "versioned-submodule";
args.defaultValue = "versioned-submodule";
};
}

View file

@ -0,0 +1,32 @@
{ name, config, lib, kubenix, subm-lib, ... }:
with lib;
let
submodule = {
imports = [ kubenix.modules.submodule ];
config.submodule = {
name = "subm";
exports = {
inherit id;
};
};
};
in {
imports = with kubenix.modules; [ test submodules ];
test = {
name = "submodules-exports";
description = "Submodules exports test";
assertions = [{
message = "should have library exported";
assertion = subm-lib.id 1 == 1;
}];
};
submodules.imports = [{
modules = [submodule];
exportAs = "subm-lib";
}];
}

View file

@ -0,0 +1,59 @@
{ name, config, lib, kubenix, ... }:
with lib;
let
submodule = { name, ... }: {
imports = [ kubenix.modules.submodule ];
config.submodule = {
name = "subm";
passthru.global.${name} = "true";
};
};
in {
imports = with kubenix.modules; [ test submodules ];
options = {
global = mkOption {
description = "Global value";
type = types.attrs;
default = {};
};
};
config = {
test = {
name = "submodules-passthru";
description = "Submodules passthru test";
assertions = [{
message = "should passthru values if passthru enabled";
assertion = hasAttr "inst1" config.global && config.global.inst1 == "true";
} {
message = "should not passthru values if passthru not enabled";
assertion = !(hasAttr "inst2" config.global);
} {
message = "should passthru by default";
assertion = hasAttr "inst3" config.global && config.global.inst3 == "true";
}];
};
submodules.imports = [{
modules = [submodule];
}];
submodules.instances.inst1 = {
submodule = "subm";
passthru.enable = true;
};
submodules.instances.inst2 = {
submodule = "subm";
passthru.enable = false;
};
submodules.instances.inst3 = {
submodule = "subm";
};
};
}

View file

@ -0,0 +1,65 @@
{ name, config, lib, kubenix, ... }:
with lib;
let
cfg = config.submodules.instances.instance;
args = cfg.config.submodule.args;
in {
imports = with kubenix.modules; [ test submodules ];
test = {
name = "submodules-simple";
description = "Simple k8s submodule test";
assertions = [{
message = "Submodule name is set";
assertion = cfg.name == "instance";
} {
message = "Submodule version is set";
assertion = cfg.version == null;
} {
message = "Submodule config has submodule definition";
assertion = cfg.config.submodule.name == "submodule";
} {
message = "Should have argument set";
assertion = args.value == "test";
} {
message = "Should have submodule name set";
assertion = args.name == "instance";
} {
message = "should have tag set";
assertion = elem "tag" (cfg.config.submodule.tags);
}];
};
submodules.propagate.enable = true;
submodules.imports = [{
module = { submodule, ... }: {
imports = [ kubenix.modules.submodule ];
options.submodule.args = {
name = mkOption {
description = "Submodule name";
type = types.str;
default = submodule.name;
};
value = mkOption {
description = "Submodule argument";
type = types.str;
};
};
config = {
submodule.name = "submodule";
submodule.tags = ["tag"];
};
};
}];
submodules.instances.instance = {
submodule = "submodule";
args = {
value = "test";
};
};
}

View file

@ -0,0 +1,71 @@
{ name, config, lib, kubenix, ... }:
with lib;
let
inst-exact = config.submodules.instances.inst-exact.config;
inst-regex = config.submodules.instances.inst-regex.config;
inst-latest = config.submodules.instances.inst-latest.config;
submodule = {
imports = [ kubenix.modules.submodule ];
options.version = mkOption {
type = types.str;
default = "undefined";
};
config.submodule.name = "subm";
};
in {
imports = with kubenix.modules; [ test submodules ];
test = {
name = "submodules-versioning";
description = "Submodules versioning test";
assertions = [{
message = "should select exact version";
assertion = inst-exact.version == "1.1.0";
} {
message = "should select regex version";
assertion = inst-regex.version == "1.2.1";
} {
message = "should select latest version";
assertion = inst-latest.version == "1.2.1";
}];
};
submodules.imports = [{
modules = [{
config.submodule.version = "1.0.0";
config.version = "1.0.0";
} submodule];
} {
modules = [{
config.submodule.version = "1.1.0";
config.version = "1.1.0";
} submodule];
} {
modules = [{
config.submodule.version = "1.2.0";
config.version = "1.2.0";
} submodule];
} {
modules = [{
config.submodule.version = "1.2.1";
config.version = "1.2.1";
} submodule];
}];
submodules.instances.inst-exact = {
submodule = "subm";
version = "1.1.0";
};
submodules.instances.inst-regex = {
submodule = "subm";
version = "~1.2.*";
};
submodules.instances.inst-latest.submodule = "subm";
}