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 definition of kubernetes resources and nix build system for building complex
kubernetes resources very easyly. kubernetes resources very easyly.
### 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 --show-trace
```
## License ## License

View file

@ -1,105 +1,31 @@
{ { pkgs ? import <nixpkgs> {}, lib ? pkgs.lib }:
pkgs ? import <nixpkgs> {}
}:
with pkgs.lib;
with import ./lib.nix { inherit pkgs; inherit (pkgs) lib; };
let 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 = [ modules = [
./kubernetes.nix configuration
./modules.nix configuration
]; ];
args = { args = {
inherit pkgs; inherit pkgs;
name = "default"; name = "default";
k8s = import ./k8s.nix {
inherit pkgs;
inherit (pkgs) lib;
};
module = null;
}; };
inherit specialArgs;
}; };
flattenResources = resources: flatten ( buildResources = configuration:
mapAttrsToList (name: resourceGroup: (evalKubernetesModules configuration).config.kubernetes.generated;
mapAttrsToList (name: resource: resource) resourceGroup
) resources
);
filterResources = resourceFilter: resources: kubenix = {
mapAttrs (groupName: resources: inherit buildResources kubenix;
(filterAttrs (name: resource:
resourceFilter groupName name resource
) resources)
) resources;
toKubernetesList = resources: { lib = lib';
kind = "List"; submodules = ./submodules.nix;
apiVersion = "v1"; k8s = ./k8s;
items = resources;
}; };
in kubenix
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; };
}

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; with lib;
rec { let
in rec {
mkOptionDefault = mkOverride 1001;
mkAllDefault = value: priority: mkAllDefault = value: priority:
if isAttrs value if isAttrs value
then mapAttrs (n: v: mkAllDefault v priority) value then mapAttrs (n: v: mkAllDefault v priority) value
@ -12,12 +15,47 @@ rec {
else mkOverride priority value; else mkOverride priority value;
moduleToAttrs = value: loadYAML = path: importJSON (pkgs.runCommand "yaml-to-json" {
if isAttrs value } "${pkgs.remarshal}/bin/remarshal -i ${path} -if yaml -of json > $out");
then mapAttrs (n: v: moduleToAttrs v) (filterAttrs (n: v: !(hasPrefix "_" n) && v != null) value)
else if isList value toYAML = config: builtins.readFile (pkgs.runCommand "to-yaml" {
then map (v: moduleToAttrs v) value 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;
};
};
}