Initial refactoring for kubenix 2.0

Implemented features:

- Improved and reimplemented submodule system, independent of
kubernetes module definitions
- Pre-generated kubernetes module definitions with explicit API
versioning support
This commit is contained in:
Jaka Hudoklin 2019-02-10 21:03:47 +01:00
parent 7287c4ed9e
commit 3dc1e615c4
No known key found for this signature in database
GPG key ID: 6A08896BFD32BD95
20 changed files with 207916 additions and 751 deletions

View file

@ -8,12 +8,13 @@ KubeNix is a kubernetes resource builder, that uses nix module system for
definition of kubernetes resources and nix build system for building complex
kubernetes resources very easyly.
### Features
## Development
- Loading and override of kubernetes json and yaml files
- Support for complex merging of kubernetes resource definitions
- No more helm stupid yaml templating, nix is a way better templating language
- Support for all kubernetes versions
### Building tests
```shell
nix-build release.nix -A test --show-trace
```
## License

View file

@ -1,105 +1,31 @@
{
pkgs ? import <nixpkgs> {}
}:
with pkgs.lib;
with import ./lib.nix { inherit pkgs; inherit (pkgs) lib; };
{ pkgs ? import <nixpkgs> {}, lib ? pkgs.lib }:
let
evalKubernetesModules = configuration: evalModules rec {
lib' = lib.extend (lib: self: import ./lib.nix { inherit lib pkgs; });
specialArgs = {
inherit kubenix;
};
evalKubernetesModules = configuration: lib'.evalModules rec {
modules = [
./kubernetes.nix
./modules.nix configuration
configuration
];
args = {
inherit pkgs;
name = "default";
k8s = import ./k8s.nix {
inherit pkgs;
inherit (pkgs) lib;
};
module = null;
};
inherit specialArgs;
};
flattenResources = resources: flatten (
mapAttrsToList (name: resourceGroup:
mapAttrsToList (name: resource: resource) resourceGroup
) resources
);
buildResources = configuration:
(evalKubernetesModules configuration).config.kubernetes.generated;
filterResources = resourceFilter: resources:
mapAttrs (groupName: resources:
(filterAttrs (name: resource:
resourceFilter groupName name resource
) resources)
) resources;
kubenix = {
inherit buildResources kubenix;
toKubernetesList = resources: {
kind = "List";
apiVersion = "v1";
items = resources;
lib = lib';
submodules = ./submodules.nix;
k8s = ./k8s;
};
removeNixOptions = resources:
map (filterAttrs (name: attr: name != "nix")) resources;
buildResources = {
configuration ? {},
resourceFilter ? groupName: name: resource: true,
withDependencies ? true
}: let
evaldConfiguration = evalKubernetesModules configuration;
allResources = moduleToAttrs (
evaldConfiguration.config.kubernetes.resources //
evaldConfiguration.config.kubernetes.customResources
);
filteredResources = filterResources resourceFilter allResources;
allDependencies = flatten (
mapAttrsToList (groupName: resources:
mapAttrsToList (name: resource: resource.nix.dependencies) resources
) filteredResources
);
resourceDependencies =
filterResources (groupName: name: resource:
elem "${groupName}/${name}" allDependencies
) allResources;
finalResources =
if withDependencies
then recursiveUpdate resourceDependencies filteredResources
else filteredResources;
resources = 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 // {
labels."kubenix/build" = listHash;
items = map (resource: recursiveUpdate resource {
metadata.labels."kubenix/build" = listHash;
}) kubernetesList.items;
};
in pkgs.writeText "resources.json" (builtins.toJSON hashedList);
in {
inherit buildResources;
test = buildResources { configuration = ./test/default.nix; };
}
in kubenix

67
examples/module/nginx.nix Normal file
View file

@ -0,0 +1,67 @@
{ config, lib, pkgs, kubenix, k8s, submodule, ... }:
with lib;
let
name = submodule.name;
in {
imports = [
kubenix.k8s
];
options.args = {
replicas = mkOption {
type = types.int;
description = "Number of nginx replicas to run";
default = 1;
};
simpleAuth = k8s.mkSecretOption {
description = "Simple auth";
default = {
key = "name";
name = "value";
};
};
image = mkOption {
description = "Image";
type = types.str;
default = "nginx";
};
};
config = {
submodule = {
name = "nginx";
version = "1.0.0";
description = "Nginx module";
passthru = {
kubernetes.objects = config.kubernetes.objects;
};
};
kubernetes.api.Deployment.nginx = {
metadata = {
name = name;
labels = {
module = config.submodule.name;
};
};
spec = {
replicas = config.args.replicas;
selector.matchLabels.app = "nginx";
template.metadata.labels.app = "nginx";
template.spec = {
containers.nginx = {
image = config.args.image;
env = {
SIMPLE_AUTH = k8s.secretToEnv config.args.simpleAuth;
};
};
};
};
};
};
}

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;
};
};
}

201
k8s/default.nix Normal file
View file

@ -0,0 +1,201 @@
{ config, lib, pkgs, ... }:
with lib;
let
isModule = hasAttr "module" config;
removeKubenixOptions = filterAttrs (name: attr: name != "kubenix");
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;
flattenResources = resources: flatten (
mapAttrsToList (groupName: versions:
mapAttrsToList (versionName: kinds:
builtins.trace versionName kinds
) versions
) resources
);
toKubernetesList = resources: {
kind = "List";
apiVersion = "v1";
items = resources;
};
apiOptions = { config, ... }: {
options = {
definitions = mkOption {
description = "Attribute set of kubernetes definitions";
};
defaults = mkOption {
description = "Kubernetes defaults";
type = types.attrsOf (types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified));
default = {};
};
resources = mkOption {
type = types.listOf (types.submodule {
options = {
group = mkOption {
description = "Group name";
type = types.str;
};
version = mkOption {
description = "Version name";
type = types.str;
};
kind = mkOption {
description = "kind name";
type = types.str;
};
plural = mkOption {
description = "Plural name";
type = types.str;
};
};
});
default = [];
};
};
};
in {
imports = [./lib.nix];
options.kubernetes.version = mkOption {
description = "Kubernetes version to use";
type = types.enum ["1.7" "1.8" "1.9" "1.10"];
default = "1.10";
};
options.kubernetes.api = mkOption {
type = types.submodule {
imports = [
(./generated + ''/v'' + config.kubernetes.version + ".nix")
apiOptions
] ++ (map (cr: {config, ...}: {
options.${cr.group}.${cr.version}.${cr.kind} = mkOption {
description = cr.description;
type = types.attrsOf (types.submodule ({name, ...}: {
imports = [cr.module];
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");
};
};
config = mkMerge ([{
apiVersion = mkOptionDefault "${cr.group}/${cr.version}";
kind = mkOptionDefault cr.kind;
metadata.name = mkOptionDefault name;
}]
++ (config.kubernetes.defaults.all or []));
}));
default = {};
};
}) config.kubernetes.customResources);
};
default = {};
};
options.kubernetes.customResources = mkOption {
default = [];
type = types.listOf (types.submodule ({config, ...}: {
options = {
group = mkOption {
description = "CRD group";
type = types.str;
};
version = mkOption {
description = "CRD version";
type = types.str;
};
kind = mkOption {
description = "CRD kind";
type = types.str;
};
plural = mkOption {
description = "CRD plural name";
type = types.str;
};
description = mkOption {
description = "CRD description";
type = types.str;
};
module = mkOption {
description = "CRD module";
default = {};
};
};
}));
};
config.kubernetes.api.resources = map (cr: {
inherit (cr) group version kind plural;
}) config.kubernetes.customResources;
options.kubernetes.propagateDefaults = mkOption {
description = "Whehter to propagate child defaults";
type = types.bool;
default = false;
};
options.kubernetes.objects = mkOption {
description = "Attribute set of kubernetes objects";
type = types.listOf types.attrs;
apply = unique;
default = [];
};
config.kubernetes.objects = flatten (map (gvk:
mapAttrsToList (name: resource:
removeKubenixOptions (moduleToAttrs resource)
) config.kubernetes.api.${gvk.group}.${gvk.version}.${gvk.kind}
) config.kubernetes.api.resources);
options.kubernetes.generated = mkOption {
type = types.package;
description = "Generated json file";
};
config.kubernetes.generated = let
kubernetesList = toKubernetesList config.kubernetes.objects;
listHash = builtins.hashString "sha1" (builtins.toJSON kubernetesList);
hashedList = kubernetesList // {
labels."kubenix/build" = listHash;
items = map (resource: recursiveUpdate resource {
metadata.labels."kubenix/build" = listHash;
}) kubernetesList.items;
};
in pkgs.writeText "resources.json" (builtins.toJSON hashedList);
}

29987
k8s/generated/v1.10.nix Normal file

File diff suppressed because it is too large Load diff

29987
k8s/generated/v1.7.nix Normal file

File diff suppressed because it is too large Load diff

29987
k8s/generated/v1.8.nix Normal file

File diff suppressed because it is too large Load diff

29987
k8s/generated/v1.9.nix Normal file

File diff suppressed because it is too large Load diff

373
k8s/generator.nix Normal file
View file

@ -0,0 +1,373 @@
{ 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" "object"];
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;
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;
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;
genResources = 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;
in nameValuePair ref {
inherit ref;
kind = path.post."x-kubernetes-group-version-kind".kind;
version = if group' != "" then "${group'}/${version'}" else version';
plural = last (splitString "/" name);
description = swagger.definitions.${ref}.description;
group = if group' == "" then "core" else group';
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;
resources = genResources swagger;
resourcesByKind = zipAttrs (mapAttrsToList (name: resource: {
${resource.kind} = resource;
}) resources);
resourcesByKindOrderedByVersion = mapAttrs (kind: resources:
reverseList (sort (r1: r2:
if compareVersions r1.version r2.version < 0
then true else false
) resources)
) resourcesByKind;
latestResourcesByKind =
mapAttrs (kind: resources: last resources) resourcesByKindOrderedByVersion;
genResourceOptions = resource: with gen; let
submoduleForDefinition' = definition: let
in submoduleForDefinition
definition.ref definition.plural definition.kind definition.group definition.version;
in mkOption {
description = resource.description;
type = types.attrsOf (submoduleForDefinition' resource);
default = {};
};
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)}
};
in {
options = {${concatStrings (mapAttrsToList (name: resource: "
\"${resource.group}\".\"${resource.version}\".\"${resource.kind}\" = ${genResourceOptions resource};
") resources)}} // {${concatStrings (mapAttrsToList (name: resource: "
\"${resource.kind}\" = ${genResourceOptions resource};
") latestResourcesByKind)}};
config = {${concatStrings (mapAttrsToList (name: resource: "
\"${resource.group}\".\"${resource.version}\".\"${resource.kind}\" = config.\"${resource.kind}\";
") latestResourcesByKind)}} // {
inherit definitions;
resources = [${concatStrings (mapAttrsToList (name: resource: "{
group = \"${resource.group}\";
version = \"${resource.version}\";
kind = \"${resource.kind}\";
plural = \"${resource.plural}\";
}") resources)}];
};
}
"

35
k8s/lib.nix Normal file
View file

@ -0,0 +1,35 @@
{ lib, ... }:
with lib;
let
k8s = {
mkSecretOption = {description ? "", default ? {}}: mkOption {
inherit description;
type = types.nullOr (types.submodule {
options = {
name = mkOption {
description = "Name of the secret where secret is stored";
type = types.str;
};
key = mkOption {
description = "Name of the key where secret is stored";
type = types.str;
};
};
config = mkDefault default;
});
default = {};
};
secretToEnv = value: {
valueFrom.secretKeyRef = {
inherit (value) name key;
};
};
};
in {
_module.args.k8s = k8s;
}

86851
k8s/specs/1.10/swagger.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,362 +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;
};
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";
};
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 = {};
};
}

52
lib.nix
View file

@ -2,7 +2,10 @@
with lib;
rec {
let
in rec {
mkOptionDefault = mkOverride 1001;
mkAllDefault = value: priority:
if isAttrs value
then mapAttrs (n: v: mkAllDefault v priority) value
@ -12,12 +15,47 @@ rec {
else mkOverride priority value;
moduleToAttrs = value:
if isAttrs value
then mapAttrs (n: v: moduleToAttrs v) (filterAttrs (n: v: !(hasPrefix "_" n) && v != null) value)
loadYAML = path: importJSON (pkgs.runCommand "yaml-to-json" {
} "${pkgs.remarshal}/bin/remarshal -i ${path} -if yaml -of json > $out");
else if isList value
then map (v: moduleToAttrs v) value
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
'');
else value;
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;
importModule = {module ? null, modules ? [module], config}: let
specialArgs = {
kubenix = import ./. { inherit pkgs lib; };
parentConfig = config;
};
isModule = hasAttr "module" config;
moduleDefinition = (evalModules {
inherit modules specialArgs;
check = false;
}).config.module.definition;
in mkOption {
description = "Module ${moduleDefinition.name} version ${moduleDefinition.version}";
type = submoduleWithSpecialArgs ({name, ...}: let
name' = if isModule then "${config.module.name}-${name}" else name;
in {
imports = modules;
module.name = mkOptionDefault name';
}) specialArgs;
default = {};
};
}

View file

@ -1,230 +0,0 @@
{ config, options, lib, pkgs, k8s, module ? null, ... }:
with lib;
with import ./lib.nix { inherit pkgs lib; };
let
globalConfig = config;
parentModule = module;
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:
let
# gets file where module is defined by looking into moduleDefinitions
# option.
file =
elemAt options.kubernetes.moduleDefinitions.files (
(findFirst (i: i > 0) 0
(imap
(i: def: if hasAttr module.module def then i else 0)
options.kubernetes.moduleDefinitions.definitions
)
) - 1
);
injectModuleAttrs = module: attrs: (
if isFunction module then args: (applyIfFunction file module args) // attrs
else if isAttrs mkOptionDefault.module then module // attrs
else module
);
in [
{
_module.args.k8s = k8s;
_module.args.name = module.name;
_module.args.module = module;
}
./kubernetes.nix
./modules.nix
(injectModuleAttrs moduleDefinition.module {_file = file;})
{
config.kubernetes.defaults.all.metadata.namespace = mkOptionDefault module.namespace;
}
] ++ config.kubernetes.defaultModuleConfiguration.all
++ (optionals (hasAttr moduleDefinition.name config.kubernetes.defaultModuleConfiguration)
config.kubernetes.defaultModuleConfiguration.${moduleDefinition.name});
prefixResources = resources: serviceName:
mapAttrs (groupName: resources:
mapAttrs' (name: resource: nameValuePair "${serviceName}-${name}" resource) resources
) resources;
prefixGroupResources = resources: serviceName:
mapAttrs' (groupName: resources:
nameValuePair "${serviceName}-${groupName}" resources
) resources;
defaultModuleConfigurationOptions = mapAttrs (name: moduleDefinition: mkOption {
description = "Module default configuration for ${name} module";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
default = [];
}) config.kubernetes.moduleDefinitions;
getModuleDefinition = name:
if hasAttr name config.kubernetes.moduleDefinitions
then config.kubernetes.moduleDefinitions.${name}
else throw ''requested kubernetes moduleDefinition with name "${name}" does not exist'';
in {
options.kubernetes.moduleDefinitions = mkOption {
description = "Attribute set of module definitions";
default = {};
type = types.attrsOf (types.submodule ({name, ...}: {
options = {
name = mkOption {
description = "Module definition name";
type = types.str;
default = name;
};
prefixResources = mkOption {
description = "Whether resources should be automatically prefixed with module name";
type = types.bool;
default = true;
};
assignAsDefaults = mkOption {
description = "Whether to assign resources as defaults, this is usefull for module that add some functionality";
type = types.bool;
default = false;
};
module = mkOption {
description = "Module definition";
};
};
}));
};
options.kubernetes.defaultModuleConfiguration = mkOption {
description = "Module default options";
type = types.submodule {
options = defaultModuleConfigurationOptions // {
all = mkOption {
description = "Module default configuration for all modules";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
default = [];
};
};
};
default = {};
};
options.kubernetes.modules = mkOption {
description = "Attribute set of modules";
default = {};
type = types.attrsOf (types.submodule ({config, name, ...}: {
options = {
name = mkOption {
description = "Module name";
type = types.str;
default = name;
};
namespace = mkOption {
description = "Namespace where to deploy module";
type = types.str;
default =
if parentModule != null
then parentModule.namespace
else "default";
};
labels = mkOption {
description = "Attribute set of module lables";
type = types.attrsOf types.str;
default = {};
};
configuration = mkOption {
description = "Module configuration";
type = submodule {
imports = mkModuleOptions (getModuleDefinition config.module) config;
};
default = {};
};
module = mkOption {
description = "Name of the module to use";
type = types.str;
default = config.name;
};
};
}));
};
config = {
kubernetes.resources = mkMerge (
mapAttrsToList (name: module: let
moduleDefinition = getModuleDefinition module.module;
moduleConfig =
if moduleDefinition.prefixResources
then prefixResources (moduleToAttrs module.configuration.kubernetes.resources) name
else moduleToAttrs module.configuration.kubernetes.resources;
in
if moduleDefinition.assignAsDefaults
then mkAllDefault moduleConfig 1000
else moduleConfig
) config.kubernetes.modules
);
kubernetes.customResources = mkMerge (
mapAttrsToList (name: module: let
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 = {
_file = head options.kubernetes.defaultModuleConfiguration.files;
config.kubernetes.version = mkDefault config.kubernetes.version;
config.kubernetes.moduleDefinitions = config.kubernetes.moduleDefinitions;
};
};
}

147
release.nix Normal file
View file

@ -0,0 +1,147 @@
{pkgs ? import <nixpkgs> {}}:
let
generate = path: import ./k8s/generator.nix {
inherit pkgs;
inherit (pkgs) lib;
inherit path;
};
kubenix = import ./. { inherit pkgs; };
in {
generate = pkgs.linkFarm "k8s-generated.nix" [{
name = "v1.7.nix";
path = generate ./k8s/specs/1.7/swagger.json;
} {
name = "v1.8.nix";
path = generate ./k8s/specs/1.8/swagger.json;
} {
name = "v1.9.nix";
path = generate ./k8s/specs/1.9/swagger.json;
} {
name = "v1.10.nix";
path = generate ./k8s/specs/1.10/swagger.json;
}];
test = kubenix.buildResources ({lib, config, kubenix, ...}: with lib; {
imports = [
kubenix.k8s
kubenix.submodules
];
config = {
kubernetes.version = "1.10";
kubernetes.api.defaults.all.metadata.namespace = mkDefault "my-namespace";
submodules.defaults = {config, parentConfig, ...}: {
kubernetes = mkIf (hasAttr "kubernetes" config) {
version = mkDefault parentConfig.kubernetes.version;
api.defaults = mkDefault parentConfig.kubernetes.api.defaults;
};
};
submodules.imports = [
# import nginx submodule
./examples/module/nginx.nix
# import of patched nginx submodule
{
modules = [./examples/module/nginx.nix ({config, ...}: {
config = {
submodule.version = mkForce "1.0-xtruder";
args.image = "xtruder/nginx";
submodules.instances.test2 = {
submodule = "test";
};
kubernetes.objects = config.submodules.instances.test2.config.kubernetes.objects;
};
})];
}
# definition of test submodule
{
module = {submodule, ...}: {
submodule.name = "test";
imports = [
kubenix.k8s
];
kubernetes.api.Pod.my-pod = {
metadata.name = submodule.name;
};
};
}
];
submodules.instances.nginx-default = {
submodule = "nginx";
};
submodules.instances.nginx-xtruder = {
submodule = "nginx";
version = "1.0-xtruder";
config = {
args.replicas = 9;
kubernetes.api.Deployment.nginx.metadata.namespace = "other-namespace";
};
};
submodules.instances.test = {
submodule = "test";
};
#kubernetes.api."cloud.google.com".v1beta1.BackendConfig.my-backend = {
#};
#modules.nginx1 = {
#args = {
#replicas = 2;
#};
#kubernetes.api.defaults.deployments = {
#spec.replicas = mkForce 3;
#};
#kubernetes.customResources = [{
#group = "cloud.google.com";
#version = "v1beta1";
#kind = "BackendConfig";
#plural = "backendconfigs";
#description = "Custom resource";
#module = {
#options.spec = {
#cdn = mkOption {
#description = "My cdn";
#type = types.str;
#default = "test";
#};
#};
#};
#}];
#};
#modules.nginx2 = {
#args = {
#replicas = 2;
#};
#kubernetes.api.defaults.deployments = {
#spec.replicas = mkForce 3;
#};
#};
kubernetes.objects = mkMerge [
config.submodules.instances.nginx-default.config.kubernetes.objects
config.submodules.instances.nginx-xtruder.config.kubernetes.objects
config.submodules.instances.test.config.kubernetes.objects
];
#kubernetes.customResources = config.modules.nginx1.kubernetes.customResources;
};
});
}

225
submodules.nix Normal file
View file

@ -0,0 +1,225 @@
{ config, kubenix, pkgs, lib, ... }:
with lib;
let
cfg = config.submodules;
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;
args.name = last loc;
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: [];
};
};
submoduleDefinitionOptions = {
options = {
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 = "0.0.0";
};
passthru = mkOption {
description = "Submodule passthru";
type = types.coercedTo types.attrs (value: [value]) (types.listOf types.attrs);
default = [];
};
};
};
submoduleOptions = {
options.submodule = mkOption {
description = "Submodule options";
type = types.submodule submoduleDefinitionOptions;
default = {};
};
};
specialArgs = cfg.specialArgs // {
parentConfig = config;
};
findModule = {name, version ? null, latest ? true}: let
matchingSubmodules = filter (el:
el.definition.name == name &&
(if version != null then el.definition.version == version else true)
) 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;
in {
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 = "Submodule defaults";
type = types.coercedTo types.unspecified (value: [value]) (types.listOf types.unspecified);
example = literalExample ''{config, ...}: {
kubernetes.version = config.kubernetes.version;
}'';
default = [];
};
submodules.propagateDefaults = mkOption {
description = "Whether to propagate defaults to submodules";
type = types.bool;
default = true;
};
submodules.propagate = mkOption {
description = "Whether to propagate defaults and imports to submodule's submodules";
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
submoduleDefinition = (evalModules {
inherit specialArgs;
modules = config.modules ++ [submoduleOptions];
check = false;
}).config.submodule;
in {
options = {
module = mkOption {
description = "Module defining submodule";
};
modules = mkOption {
description = "List of modules defining submodule";
type = types.listOf types.unspecified;
default = [config.module];
};
definition = mkOption {
type = types.submodule submoduleDefinitionOptions;
default = submoduleDefinition;
};
};
})
)
);
default = [];
};
submodules.instances = mkOption {
description = "Attribute set of submodule instances";
type = types.attrsOf (types.submodule ({name, config, ...}: let
submodule = findModule {
name = config.submodule;
version = config.version;
};
submoduleDefinition = submodule.definition;
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";
type = types.nullOr types.str;
default = null;
};
config = mkOption {
description = "Submodule instance ${config.name} for ${submoduleDefinition.name}:${submoduleDefinition.version} config";
type = submoduleWithSpecialArgs ({...}: {
imports = submodule.modules ++ cfg.defaults ++ [submoduleOptions ./submodules.nix];
_module.args.submodule = {
name = config.name;
};
}) specialArgs;
default = {};
};
};
}));
};
default = {};
};
config = {
submodules.specialArgs.kubenix = kubenix;
submodules.defaults = mkIf cfg.propagate {
submodules.defaults = cfg.defaults;
submodules.imports = cfg.imports;
};
};
}