diff --git a/.travis.yml b/.travis.yml index 87ddabe..4c6be18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ script: - nix-env -iA nixpkgs.jq - nix build -f ./release.nix test-results --arg e2e false - cat result | jq '.' -- nix eval -f ./release.nix --arg e2e false tests-check +- nix eval -f ./release.nix --arg e2e false test-check diff --git a/README.md b/README.md index 359bce6..9dedf63 100644 --- a/README.md +++ b/README.md @@ -13,19 +13,20 @@ kubernetes resources very easyly. ### Building tests ```shell -nix-build release.nix -A tests.results --show-trace +nix-build release.nix -A tests-results --show-trace ``` **Building single e2e test** ``` -nix-build release.nix -A tests.tests.v1_10.testing.testsByName..script +cd tests +nix-build release.nix -A tests.k8s-1_10.testsByName..test ``` **Debugging e2e test** ``` -nix-build release.nix -A tests.tests.v1_10.testing.testsByName..script.driver +nix-build release.nix -A tests.k8s-1_10.testsByName..test.driver resut/bin/nixos-test-driver testScript; ``` diff --git a/default.nix b/default.nix index 5769833..788119f 100644 --- a/default.nix +++ b/default.nix @@ -32,6 +32,5 @@ let inherit evalModules modules; lib = kubenixLib; - module = modules.module; }; in kubenix diff --git a/examples/nginx-deployment/module.nix b/examples/nginx-deployment/module.nix index e1da9fa..d7e1606 100644 --- a/examples/nginx-deployment/module.nix +++ b/examples/nginx-deployment/module.nix @@ -5,7 +5,7 @@ with lib; let nginx = pkgs.callPackage ./image.nix { }; in { - imports = [ kubenix.module ]; + imports = with kubenix.modules; [ k8s docker ]; docker.images.nginx.image = nginx; diff --git a/lib/extra.nix b/lib/extra.nix index a165d41..dd6e583 100644 --- a/lib/extra.nix +++ b/lib/extra.nix @@ -12,8 +12,6 @@ rec { else value; - mkOptionDefault = mkOverride 1001; - mkAllDefault = value: priority: if isAttrs value then mapAttrs (n: v: mkAllDefault v priority) value diff --git a/modules/base.nix b/modules/base.nix new file mode 100644 index 0000000..22ee137 --- /dev/null +++ b/modules/base.nix @@ -0,0 +1,19 @@ +{ config, lib, ... }: + +with lib; + +{ + options._module.features = mkOption { + description = "List of features exposed by module"; + type = types.listOf types.str; + default = []; + }; + + options.kubenix = { + project = mkOption { + description = "Name of the project"; + type = types.str; + default = "kubenix"; + }; + }; +} diff --git a/modules/default.nix b/modules/default.nix index 6162323..cb34fbd 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -2,9 +2,9 @@ k8s = ./k8s.nix; istio = ./istio.nix; submodules = ./submodules.nix; + submodule = ./submodule.nix; helm = ./helm.nix; docker = ./docker.nix; - metacontroller = ./metacontroller; testing = ./testing.nix; test = ./test.nix; module = ./module.nix; diff --git a/modules/docker.nix b/modules/docker.nix index ffff7f4..1e02ce0 100644 --- a/modules/docker.nix +++ b/modules/docker.nix @@ -3,61 +3,77 @@ with lib; let - globalConfig = config; + cfg = config.docker; in { - options.docker.registry.url = mkOption { - description = "Default registry url where images are published"; - type = types.str; - default = ""; + options.docker = { + registry.url = mkOption { + description = "Default registry url where images are published"; + type = types.str; + default = ""; + }; + + images = mkOption { + description = "Attribute set of docker images that should be published"; + type = types.attrsOf (types.submodule ({ name, config, ... }: { + options = { + image = mkOption { + description = "Docker image to publish"; + type = types.nullOr types.package; + default = null; + }; + + name = mkOption { + description = "Desired docker image name"; + type = types.str; + default = builtins.unsafeDiscardStringContext config.image.imageName; + }; + + tag = mkOption { + description = "Desired docker image tag"; + type = types.str; + default = builtins.unsafeDiscardStringContext config.image.imageTag; + }; + + registry = mkOption { + description = "Docker registry url where image is published"; + type = types.str; + default = cfg.registry.url; + }; + + path = mkOption { + description = "Full docker image path"; + type = types.str; + default = + if config.registry != "" + then "${config.registry}/${config.name}:${config.tag}" + else "${config.name}:${config.tag}"; + }; + }; + })); + default = {}; + }; + + export = mkOption { + description = "List of images to export"; + type = types.listOf (types.package); + default = []; + }; }; - options.docker.images = mkOption { - description = "Attribute set of docker images that should be published"; - type = types.attrsOf (types.submodule ({ name, config, ... }: { - options = { - image = mkOption { - description = "Docker image to publish"; - type = types.nullOr types.package; - default = null; - }; + config = { + _module.features = ["docker"]; - name = mkOption { - description = "Desired docker image name"; - type = types.str; - default = builtins.unsafeDiscardStringContext config.image.imageName; - }; + docker.export = mkMerge [ + (mapAttrsToList (_: i: i.image) + (filterAttrs (_: i: i.registry != null) config.docker.images)) - tag = mkOption { - description = "Desired docker image tag"; - type = types.str; - default = builtins.unsafeDiscardStringContext config.image.imageTag; - }; - - registry = mkOption { - description = "Docker registry url where image is published"; - type = types.str; - default = globalConfig.docker.registry.url; - }; - - path = mkOption { - description = "Full docker image path"; - type = types.str; - default = - if config.registry != "" - then "${config.registry}/${config.name}:${config.tag}" - else "${config.name}:${config.tag}"; - }; - }; - })); - default = {}; + # passthru of docker exported images if passthru is enabled on submodule + # and submodule has docker module loaded + (flatten (mapAttrsToList (_: submodule: + optionals + (submodule.passthru.enable && (elem "docker" submodule.config._module.features)) + submodule.config.docker.export + ) config.submodules.instances)) + ]; }; - - options.docker.export = mkOption { - description = "List of images to export"; - type = types.listOf (types.package); - default = []; - }; - - config.docker.export = mapAttrsToList (_: i: i.image) - (filterAttrs (_: i: i.registry != null)config.docker.images); } diff --git a/modules/helm.nix b/modules/helm.nix index b280efe..8934a2a 100644 --- a/modules/helm.nix +++ b/modules/helm.nix @@ -27,9 +27,6 @@ let in { imports = [ ./k8s.nix ]; - # expose helm helper methods as module argument - config._module.args.helm = import ../lib/helm { inherit pkgs; }; - options.kubernetes.helm = { instances = mkOption { description = "Attribute set of helm instances"; @@ -84,7 +81,7 @@ in { }; config.overrides = mkIf (config.overrideNamespace && config.namespace != null) [{ - metadata.namespace = mkDefault config.namespace; + metadata.namespace = config.namespace; }]; config.objects = importJSON (helm.chart2json { @@ -94,14 +91,19 @@ in { }; }; - config.kubernetes.api = mkMerge (flatten (mapAttrsToList (_: instance: - map (object: let - apiVersion = parseApiVersion object.apiVersion; - name = object.metadata.name; - in { - "${apiVersion.group}"."${apiVersion.version}".${object.kind}."${name}" = mkMerge ([ - object - ] ++ instance.overrides); - }) instance.objects - ) cfg.instances)); + config = { + # expose helm helper methods as module argument + _module.args.helm = import ../lib/helm { inherit pkgs; }; + + kubernetes.api = mkMerge (flatten (mapAttrsToList (_: instance: + map (object: let + apiVersion = parseApiVersion object.apiVersion; + name = object.metadata.name; + in { + "${apiVersion.group}"."${apiVersion.version}".${object.kind}."${name}" = mkMerge ([ + object + ] ++ instance.overrides); + }) instance.objects + ) cfg.instances)); + }; } diff --git a/modules/k8s.nix b/modules/k8s.nix index 5a9f4c5..d315c4c 100644 --- a/modules/k8s.nix +++ b/modules/k8s.nix @@ -68,6 +68,12 @@ let default = null; }; + propagate = mkOption { + description = "Whether to propagate default"; + type = types.bool; + default = false; + }; + default = mkOption { description = "Default to apply"; type = types.unspecified; @@ -164,107 +170,162 @@ let }; }) cfg.customResources; in { - # expose k8s helper methods as module argument - config._module.args.k8s = import ../lib/k8s.nix { inherit lib; }; + imports = [ ./base.nix ./submodules.nix ]; - options.kubernetes.version = mkOption { - description = "Kubernetes version to use"; - type = types.enum ["1.7" "1.8" "1.9" "1.10" "1.11" "1.12" "1.13"]; - default = "1.13"; - }; - - options.kubernetes.resourceOrder = mkOption { - description = "Preffered resource order"; - type = types.listOf types.str; - default = [ - "CustomResourceDefinition" - "Namespace" - ]; - }; - - options.kubernetes.api = mkOption { - type = types.submodule { - imports = [ - (./generated + ''/v'' + cfg.version + ".nix") - apiOptions - ] ++ customResourceOptions; + options.kubernetes = { + version = mkOption { + description = "Kubernetes version to use"; + type = types.enum ["1.7" "1.8" "1.9" "1.10" "1.11" "1.12" "1.13"]; + default = "1.13"; }; - default = {}; - }; - options.kubernetes.customResources = mkOption { - default = []; - description = "List of custom resource definitions to make API for"; - type = types.listOf (types.submodule ({config, ...}: { - options = { - group = mkOption { - description = "Custom resource definition group"; - type = types.str; - }; + namespace = mkOption { + description = "Default namespace where to deploy kubernetes resources"; + type = types.str; + default = "default"; + }; - version = mkOption { - description = "Custom resource definition version"; - type = types.str; - }; + resourceOrder = mkOption { + description = "Preffered resource order"; + type = types.listOf types.str; + default = [ + "CustomResourceDefinition" + "Namespace" + ]; + }; - kind = mkOption { - description = "Custom resource definition kind"; - type = types.str; - }; - - resource = mkOption { - description = "Custom resource definition resource name"; - type = types.nullOr types.str; - default = null; - }; - - description = mkOption { - description = "Custom resource definition description"; - type = types.str; - default = ""; - }; - - module = mkOption { - description = "Custom resource definition module"; - default = types.unspecified; - }; - - alias = mkOption { - description = "Alias to create for API"; - type = types.nullOr types.str; - default = null; - }; + api = mkOption { + type = types.submodule { + imports = [ + (./generated + ''/v'' + cfg.version + ".nix") + apiOptions + ] ++ customResourceOptions; }; - })); + default = {}; + }; + + customResources = mkOption { + default = []; + description = "List of custom resource definitions to make API for"; + type = types.listOf (types.submodule ({config, ...}: { + options = { + group = mkOption { + description = "Custom resource definition group"; + type = types.str; + }; + + version = mkOption { + description = "Custom resource definition version"; + type = types.str; + }; + + kind = mkOption { + description = "Custom resource definition kind"; + type = types.str; + }; + + resource = mkOption { + description = "Custom resource definition resource name"; + type = types.nullOr types.str; + default = null; + }; + + description = mkOption { + description = "Custom resource definition description"; + type = types.str; + default = ""; + }; + + module = mkOption { + description = "Custom resource definition module"; + default = types.unspecified; + }; + + alias = mkOption { + description = "Alias to create for API"; + type = types.nullOr types.str; + default = null; + }; + }; + })); + }; + + objects = mkOption { + description = "List of generated kubernetes objects"; + type = types.listOf types.attrs; + apply = items: sort (r1: r2: + if elem r1.kind cfg.resourceOrder && elem r2.kind cfg.resourceOrder + then indexOf cfg.resourceOrder r1.kind < indexOf cfg.resourceOrder r2.kind + else if elem r1.kind cfg.resourceOrder then true else false + ) (moduleToAttrs (unique items)); + default = []; + }; + + generated = mkOption { + description = "Generated kubernetes list object"; + type = types.attrs; + }; }; - config.kubernetes.api.resources = map (cr: { - inherit (cr) group version kind resource; - }) cfg.customResources; + config = { + # expose k8s helper methods as module argument + _module.args.k8s = import ../lib/k8s.nix { inherit lib; }; - options.kubernetes.objects = mkOption { - description = "List of generated kubernetes objects"; - type = types.listOf types.attrs; - apply = items: sort (r1: r2: - if elem r1.kind cfg.resourceOrder && elem r2.kind cfg.resourceOrder - then indexOf cfg.resourceOrder r1.kind < indexOf cfg.resourceOrder r2.kind - else if elem r1.kind cfg.resourceOrder then true else false - ) (moduleToAttrs (unique items)); - default = []; - }; + _module.features = [ "k8s" ]; - config.kubernetes.objects = flatten (map (gvk: - mapAttrsToList (name: resource: - removeKubenixOptions (moduleToAttrs resource) - ) cfg.api.${gvk.group}.${gvk.version}.${gvk.kind} - ) cfg.api.resources); + kubernetes.api.resources = map (cr: { + inherit (cr) group version kind resource; + }) cfg.customResources; - options.kubernetes.generated = mkOption { - description = "Generated kubernetes list object"; - type = types.attrs; - }; + kubernetes.objects = mkMerge [ + # gvk resources + (flatten (map (gvk: + mapAttrsToList (name: resource: + removeKubenixOptions (moduleToAttrs resource) + ) cfg.api.${gvk.group}.${gvk.version}.${gvk.kind} + ) cfg.api.resources)) - config.kubernetes.generated = k8s.mkHashedList { - items = config.kubernetes.objects; + # passthru of child kubernetes objects if passthru is enabled on submodule + # and submodule has k8s module loaded + (flatten (mapAttrsToList (_: submodule: + optionals + (submodule.passthru.enable && (elem "k8s" submodule.config._module.features)) + submodule.config.kubernetes.objects + ) config.submodules.instances)) + ]; + + kubernetes.generated = k8s.mkHashedList { + items = config.kubernetes.objects; + labels."kubenix/project-name" = config.kubenix.project; + }; + + kubernetes.api.defaults = [{ + default = { + metadata.namespace = mkDefault config.kubernetes.namespace; + metadata.labels = mkMerge [ + { + "kubenix/project-name" = config.kubenix.project; + } + + # if we are inside submodule, define additional labels + (mkIf (elem "submodule" config._module.features) { + "kubenix/module-name" = config.submodule.name; + "kubenix/module-version" = config.submodule.version; + }) + ]; + }; + }]; + + submodules.defaults = [{ + features = [ "k8s" ]; + default = { config, name, ... }: { + # propagate kubernetes version and namespace + kubernetes.version = mkDefault cfg.version; + kubernetes.namespace = mkDefault cfg.namespace; + + # propagate defaults if default propagation is enabled + kubernetes.api.defaults = filter (default: default.propagate) cfg.api.defaults; + }; + }]; }; } diff --git a/modules/metacontroller/compositecontroller.nix b/modules/metacontroller/compositecontroller.nix deleted file mode 100644 index ef30574..0000000 --- a/modules/metacontroller/compositecontroller.nix +++ /dev/null @@ -1,139 +0,0 @@ -{ config, lib, ... }: - -with lib; - -{ - options = { - parentResource = { - apiVersion = mkOption { - description = "Parent resource apiVersion"; - type = types.str; - example = "apps/v1"; - }; - - resource = mkOption { - description = "The canonical, lowercase, plural name of the parent resource"; - type = types.str; - example = "deployments"; - }; - - revisionHistory = mkOption { - description = "A list of field path strings specifying which parent fields trigger rolling updates of children"; - type = types.listOf types.str; - default = ["spec"]; - example = ["spec.template"]; - }; - }; - - childResources = mkOption { - description = "A list of resource rules specifying the child resources"; - type = types.listOf (types.submodule ({ config, ... }: { - options = { - apiVersion = mkOption { - description = "The API group/version of the child resource, or just version for core APIs"; - type = types.str; - example = "apps/v1"; - }; - - resource = mkOption { - description = "The canonical, lowercase, plural name of the child resource"; - type = types.str; - example = "deployments"; - }; - - updateStrategy = { - method = mkOption { - description = '' - A string indicating the overall method that should be used for updating this type of child resource. - The default is OnDelete, which means don't try to update children that already exist. - - - OnDelete: Don't update existing children unless they get deleted by some other agent. - - Recreate: Immediately delete any children that differ from the desired state, and recreate them in the desired state. - - InPlace: Immediately update any children that differ from the desired state. - - RollingRecreate: Delete each child that differs from the desired state, one at a time, - and recreate each child before moving on to the next one. - Pause the rollout if at any time one of the children that have already been updated fails one or more status checks. - - RollingInPlace: Update each child that differs from the desired state, one at a time. Pause the rollout if at any time - one of the children that have already been updated fails one or more status checks. - ''; - type = types.enum [ - "OnDelete" - "Recreate" - "InPlace" - "RollingRecreate" - "RollingInPlace" - ]; - default = "OnDelete"; - }; - - statusChecks.conditions = mkOption { - description = '' - A list of status condition checks that must all pass on already-updated - children for the rollout to continue. - ''; - type = types.listOf (types.submodule ({ config, ... }: { - options = { - type = mkOption { - description = "A string specifying the status condition type to check."; - type = types.str; - }; - - status = mkOption { - description = '' - A string specifying the required status of the given status condition. - If none is specified, the condition's status is not checked. - ''; - type = types.str; - default = ""; - }; - - reason = mkOption { - description = '' - A string specifying the required reason of the given status condition. - If none is specified, the condition's reason is not checked. - ''; - type = types.str; - default = ""; - }; - }; - })); - default = []; - }; - }; - }; - })); - default = []; - }; - - resyncPeriodSeconds = mkOption { - description = '' - How often, in seconds, you want every parent object to be resynced, - even if no changes are detected. - ''; - type = types.int; - default = 0; - }; - - generateSelector = mkOption { - description = '' - If true, ignore the selector in each parent object and instead generate - a unique selector that prevents overlap with other objects. - ''; - type = types.bool; - default = false; - }; - - hooks = { - sync.webhook.url = mkOption { - description = "Webhook URL where to send sync request"; - type = types.str; - }; - - finalize.webhook.url = mkOption { - description = "Webhook URL where to send finalize request"; - type = types.str; - default = ""; - }; - }; - }; -} diff --git a/modules/metacontroller/default.nix b/modules/metacontroller/default.nix deleted file mode 100644 index 4ddd28e..0000000 --- a/modules/metacontroller/default.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ config, lib, ... }: - -with lib; - -{ - imports = [ ../k8s.nix ]; - - options.metacontroller = { - compositeControllers = mkOption { - type = types.attrsOf (types.submodule ({ name, config, ... }: { - imports = [ ./compositecontroller.nix ]; - - options = { - name = mkOption { - description = "Name of the composite controller"; - type = types.str; - default = name; - }; - }; - })); - default = {}; - }; - }; - - config = { - kubernetes.customResources = [{ - group = "metacontroller.k8s.io"; - version = "v1alpha1"; - kind = "CompositeController"; - resource = "compositecontrollers"; - description = "Composite controller"; - alias = "compositecontrollers"; - module.imports = [ ./compositecontroller.nix ]; - }]; - }; -} diff --git a/modules/module.nix b/modules/module.nix deleted file mode 100644 index fda3eeb..0000000 --- a/modules/module.nix +++ /dev/null @@ -1,75 +0,0 @@ -# module.nix defines default kubenix module with additional helper options -# and preincluded kubenix module definitions for kubernetes, docker and -# kubenix submodules - -{ config, lib, ... }: - -with lib; - -let - parentConfig = config; -in { - imports = [ ./k8s.nix ./docker.nix ./submodules.nix ]; - - options = { - kubenix.release = mkOption { - description = "Name of the release"; - type = types.str; - default = "default"; - }; - - kubernetes.propagateDefaults = mkOption { - description = "Whether to propagate child defaults to submodules"; - type = types.bool; - default = true; - }; - - submodules.instances = mkOption { - type = types.attrsOf (types.submodule ({config, ...}: { - options = { - namespace = mkOption { - description = "Default kubernetes namespace"; - type = types.str; - default = "default"; - }; - }; - - config.config = { - kubernetes.api.defaults = [{ - default.metadata.namespace = mkDefault config.namespace; - }]; - }; - })); - }; - }; - - config = { - submodules.defaults = [{ - default = { - imports = [ ./module.nix ]; - kubernetes.version = mkDefault config.kubernetes.version; - kubernetes.api.defaults = - mkIf config.kubernetes.propagateDefaults config.kubernetes.api.defaults; - }; - } { - default = ({config, ...}: { - kubenix.release = parentConfig.kubenix.release; - kubernetes.api.defaults = [{ - default.metadata.labels = { - "kubenix/release-name" = config.kubenix.release; - "kubenix/module-name" = config.submodule.name; - "kubenix/module-version" = config.submodule.version; - }; - }]; - }); - }]; - - kubernetes.objects = mkMerge (mapAttrsToList (_: submodule: - submodule.config.kubernetes.objects - ) config.submodules.instances); - - docker.export = mkMerge (mapAttrsToList (_: submodule: - submodule.config.docker.export - ) config.submodules.instances); - }; -} diff --git a/modules/submodule.nix b/modules/submodule.nix new file mode 100644 index 0000000..a2e1a70 --- /dev/null +++ b/modules/submodule.nix @@ -0,0 +1,40 @@ +{ config, lib, ... }: + +with lib; + +{ + imports = [ ./base.nix ]; + + options.submodule = { + name = mkOption { + description = "Module name"; + type = types.str; + }; + + description = mkOption { + description = "Module description"; + type = types.str; + default = ""; + }; + + version = mkOption { + description = "Module version"; + type = types.str; + default = "1.0.0"; + }; + + tags = mkOption { + description = "List of submodule tags"; + type = types.listOf types.str; + default = []; + }; + + passthru = mkOption { + description = "Attribute set to passthru to parent"; + default = {}; + type = types.attrs; + }; + }; + + config._module.features = ["submodule"]; +} diff --git a/modules/submodules.nix b/modules/submodules.nix index 9ccda4a..9e56f63 100644 --- a/modules/submodules.nix +++ b/modules/submodules.nix @@ -4,13 +4,18 @@ with lib; let cfg = config.submodules; + parentConfig = config; - getDefaults = name: tags: + getDefaults = {name, tags, features}: catAttrs "default" (filter (submodule: (submodule.name == null || submodule.name == name) && ( (length submodule.tags == 0) || (length (intersectLists submodule.tags tags)) > 0 + ) && + ( + (length submodule.features == 0) || + (length (intersectLists submodule.features features)) > 0 ) ) config.submodules.defaults); @@ -59,35 +64,6 @@ let }; }; - submoduleDefinitionOptions = { - name = mkOption { - description = "Module name"; - type = types.str; - }; - - description = mkOption { - description = "Module description"; - type = types.str; - default = ""; - }; - - version = mkOption { - description = "Module version"; - type = types.str; - default = "1.0.0"; - }; - - tags = mkOption { - description = "List of submodule tags"; - type = types.listOf types.str; - default = []; - }; - }; - - submoduleOptions = { - options.submodule = submoduleDefinitionOptions; - }; - specialArgs = cfg.specialArgs // { parentConfig = config; }; @@ -113,6 +89,8 @@ let else head versionSortedSubmodules; in matchingModule; in { + imports = [ ./base.nix ]; + options = { submodules.specialArgs = mkOption { description = "Special args to pass to submodules. These arguments can be used for imports"; @@ -136,6 +114,12 @@ in { default = []; }; + features = mkOption { + description = "List of features that submodule has to have to apply defaults"; + type = types.listOf types.str; + default = []; + }; + default = mkOption { description = "Default to apply to submodule instance"; type = types.unspecified; @@ -146,10 +130,10 @@ in { default = []; }; - submodules.propagate = mkOption { - description = "Whether to propagate defaults and imports to submodule's submodules"; + submodules.propagate.enable = mkOption { + description = "Whether to propagate defaults and imports from parent to child"; type = types.bool; - default = false; + default = true; }; submodules.imports = mkOption { @@ -159,11 +143,16 @@ in { types.path (module: {inherit module;}) (types.submodule ({name, config, ...}: let - submoduleDefinition = (evalModules { + evaledSubmodule' = (evalModules { inherit specialArgs; - modules = config.modules ++ [submoduleOptions]; + modules = config.modules ++ [ ./base.nix ]; check = false; - }).config.submodule; + }).config; + + evaledSubmodule = + if (!(elem "submodule" evaledSubmodule'._module.features)) + then throw "no submodule defined" + else evaledSubmodule'; in { options = { module = mkOption { @@ -177,11 +166,23 @@ in { default = [config.module]; }; - definition = submoduleDefinitionOptions; + features = mkOption { + description = "List of features exposed by submodule"; + type = types.listOf types.str; + }; + + definition = mkOption { + description = "Submodule definition"; + type = types.attrs; + }; }; - config.definition = { - inherit (submoduleDefinition) name description version tags; + config = { + definition = { + inherit (evaledSubmodule.submodule) name description version tags; + }; + + features = evaledSubmodule._module.features; }; }) ) @@ -203,7 +204,11 @@ in { submoduleDefinition = submodule.definition; # submodule defaults - defaults = getDefaults submoduleDefinition.name submoduleDefinition.tags; + defaults = getDefaults { + name = submoduleDefinition.name; + tags = submoduleDefinition.tags; + features = submodule.features; + }; in { options = { name = mkOption { @@ -227,10 +232,16 @@ in { default = null; }; + passthru.enable = mkOption { + description = "Whether to passthru submodule resources"; + type = types.bool; + default = true; + }; + config = mkOption { description = "Submodule instance ${config.name} for ${submoduleDefinition.name}:${submoduleDefinition.version} config"; type = submoduleWithSpecialArgs ({...}: { - imports = submodule.modules ++ defaults ++ [submoduleOptions]; + imports = submodule.modules ++ defaults ++ [ ./base.nix ]; _module.args.pkgs = pkgs; _module.args.name = config.name; _module.args.submodule = config; @@ -243,16 +254,31 @@ in { default = {}; }; - config = { - submodules.specialArgs.kubenix = kubenix; - submodules.defaults = [(mkIf cfg.propagate { - default = { - imports = [./submodules.nix]; - submodules = { - defaults = cfg.defaults; - imports = cfg.imports; + config = mkMerge [ + { + _module.features = ["submodules"]; + + submodules.specialArgs.kubenix = kubenix; + + # passthru kubenix.project to submodules + submodules.defaults = [{ + default = { + kubenix.project = parentConfig.kubenix.project; }; - }; - })]; - }; + }]; + } + + (mkIf cfg.propagate.enable { + # if propagate is enabled and submodule has submodules included propagage defaults and imports + submodules.defaults = [{ + features = ["submodules"]; + default = { + submodules = { + defaults = cfg.defaults; + imports = cfg.imports; + }; + }; + }]; + }) + ]; } diff --git a/modules/testing.nix b/modules/testing.nix index 9a86867..082143c 100644 --- a/modules/testing.nix +++ b/modules/testing.nix @@ -4,7 +4,6 @@ with lib; let cfg = config.testing; - parentConfig = config; nixosTesting = import { inherit pkgs; @@ -110,7 +109,9 @@ let testOptions = {config, ...}: let modules = [config.module ./test.nix { - config._module.args.test = config; + config._module.args = { + test = config; + } // cfg.args; }] ++ cfg.defaults; test = (kubenix.evalModules { @@ -229,6 +230,12 @@ in { apply = tests: filter (test: test.enable) tests; }; + testing.args = mkOption { + description = "Attribute set of extra args passed to tests"; + type = types.attrs; + default = {}; + }; + testing.testsByName = mkOption { description = "Tests by name"; type = types.attrsOf types.attrs; @@ -249,6 +256,7 @@ in { tests = map (test: { inherit (test) name description success test; assertions = moduleToAttrs test.assertions; + generated = test.generated; }) (filter (test: test.enable) cfg.tests); }; }; diff --git a/release.nix b/release.nix index c5cb613..d11cf67 100644 --- a/release.nix +++ b/release.nix @@ -100,7 +100,7 @@ in rec { results = mapAttrs (_: test: test.result) tests; }); - tests-check = + test-check = if !(all (test: test.success) (attrValues tests)) then throw "tests failed" else true; diff --git a/tests/default.nix b/tests/default.nix index 56f3ce7..cac7ec7 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -26,19 +26,16 @@ let ./k8s/1.13/crd.nix ./k8s/defaults.nix ./k8s/order.nix + ./k8s/submodule.nix ./helm/simple.nix ./istio/bookinfo.nix ./submodules/simple.nix ./submodules/defaults.nix ./submodules/versioning.nix - ./module.nix - ./metacontroller/compositecontroller.nix ]; - testing.defaults = ({kubenix, ...}: { - imports = [kubenix.modules.k8s]; - kubernetes.version = k8sVersion; - _module.args.images = images; - }); + testing.args = { + inherit images k8sVersion; + }; } ]; args = { diff --git a/tests/helm/simple.nix b/tests/helm/simple.nix index 85e04c2..ba20ec2 100644 --- a/tests/helm/simple.nix +++ b/tests/helm/simple.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, kubenix, helm, ... }: +{ config, lib, pkgs, kubenix, helm, k8sVersion, ... }: with lib; with kubenix.lib; @@ -60,6 +60,8 @@ in { ''; }; + kubernetes.version = k8sVersion; + kubernetes.api.namespaces.test = {}; kubernetes.helm.instances.app-psql = { diff --git a/tests/istio/bookinfo.nix b/tests/istio/bookinfo.nix index 269bca8..ba46713 100644 --- a/tests/istio/bookinfo.nix +++ b/tests/istio/bookinfo.nix @@ -1,4 +1,4 @@ -{ config, kubenix, ... }: +{ config, kubenix, k8sVersion, ... }: { imports = with kubenix.modules; [ test k8s istio ]; @@ -8,6 +8,8 @@ description = "Simple istio bookinfo application (WIP)"; }; + kubernetes.version = k8sVersion; + kubernetes.api."networking.istio.io"."v1alpha3" = { Gateway."bookinfo-gateway" = { spec = { diff --git a/tests/k8s/1.13/crd.nix b/tests/k8s/1.13/crd.nix index 4b3b20d..837b122 100644 --- a/tests/k8s/1.13/crd.nix +++ b/tests/k8s/1.13/crd.nix @@ -1,4 +1,4 @@ -{ config, lib, kubenix, ... }: +{ config, lib, kubenix, k8sVersion, ... }: with lib; @@ -17,6 +17,8 @@ in { }]; }; + kubernetes.version = k8sVersion; + kubernetes.api.customresourcedefinitions.crontabs = { metadata.name = "crontabs.stable.example.com"; spec = { diff --git a/tests/k8s/crd.nix b/tests/k8s/crd.nix index 2fea676..aea3176 100644 --- a/tests/k8s/crd.nix +++ b/tests/k8s/crd.nix @@ -1,4 +1,4 @@ -{ config, lib, kubenix, pkgs, ... }: +{ config, lib, kubenix, pkgs, k8sVersion, ... }: with lib; @@ -22,6 +22,8 @@ in { ''; }; + kubernetes.version = k8sVersion; + kubernetes.api.customresourcedefinitions.crontabs = { metadata.name = "crontabs.stable.example.com"; spec = { diff --git a/tests/k8s/defaults.nix b/tests/k8s/defaults.nix index 8f4a8fe..a2d1c67 100644 --- a/tests/k8s/defaults.nix +++ b/tests/k8s/defaults.nix @@ -1,4 +1,4 @@ -{ config, lib, kubenix, ... }: +{ config, lib, kubenix, k8sVersion, ... }: with lib; @@ -23,6 +23,8 @@ in { }]; }; + kubernetes.version = k8sVersion; + kubernetes.api.pods.pod1 = {}; kubernetes.api.pods.pod2 = { diff --git a/tests/k8s/deployment.nix b/tests/k8s/deployment.nix index 6049b8a..040aa76 100644 --- a/tests/k8s/deployment.nix +++ b/tests/k8s/deployment.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, kubenix, images, ... }: +{ config, lib, pkgs, kubenix, images, k8sVersion, ... }: with lib; @@ -41,6 +41,8 @@ in { docker.images.nginx.image = image; + kubernetes.version = k8sVersion; + kubernetes.api.deployments.nginx = { spec = { replicas = 10; diff --git a/tests/k8s/order.nix b/tests/k8s/order.nix index 314e820..8409269 100644 --- a/tests/k8s/order.nix +++ b/tests/k8s/order.nix @@ -1,4 +1,4 @@ -{ config, lib, kubenix, pkgs, ... }: +{ config, lib, kubenix, pkgs, k8sVersion, ... }: with lib; @@ -20,6 +20,8 @@ in { }]; }; + kubernetes.version = k8sVersion; + kubernetes.api.customresourcedefinitions.crontabs = { metadata.name = "crontabs.stable.example.com"; spec = { diff --git a/tests/k8s/simple.nix b/tests/k8s/simple.nix index 107b788..0f8e442 100644 --- a/tests/k8s/simple.nix +++ b/tests/k8s/simple.nix @@ -1,4 +1,4 @@ -{ config, kubenix, ... }: +{ config, kubenix, k8sVersion, ... }: let cfg = config.kubernetes.api.pods.nginx; @@ -17,5 +17,7 @@ in { }]; }; + kubernetes.version = k8sVersion; + kubernetes.api.pods.nginx = {}; } diff --git a/tests/k8s/submodule.nix b/tests/k8s/submodule.nix new file mode 100644 index 0000000..b638637 --- /dev/null +++ b/tests/k8s/submodule.nix @@ -0,0 +1,56 @@ +{ name, config, lib, kubenix, images, ... }: + +with lib; + +let + cfg = config.submodules.instances.passthru; +in { + imports = with kubenix.modules; [ test submodules k8s docker ]; + + test = { + name = "k8s-submodule"; + description = "Simple k8s submodule test"; + assertions = [{ + message = "Submodule has correct name set"; + assertion = (head config.kubernetes.objects).metadata.name == "passthru"; + } { + message = "Should expose docker image"; + assertion = (head config.docker.export).imageName == "xtruder/nginx"; + }]; + }; + + kubenix.project = "test-release"; + + kubernetes.namespace = "test-namespace"; + + submodules.imports = [{ + module = {name, config, ...}: { + imports = with kubenix.modules; [ submodule k8s docker ]; + + config = { + submodule.name = "test-submodule"; + + kubernetes.api.pods.nginx = { + metadata.name = name; + spec.containers.nginx.image = config.docker.images.nginx.path; + }; + + docker.images.nginx.image = images.nginx; + }; + }; + }]; + + kubernetes.api.defaults = [{ + propagate = true; + default.metadata.labels.my-label = "my-value"; + }]; + + submodules.instances.passthru = { + submodule = "test-submodule"; + }; + + submodules.instances.no-passthru = { + submodule = "test-submodule"; + passthru.enable = false; + }; +} diff --git a/tests/submodules/defaults.nix b/tests/submodules/defaults.nix index 32d0838..5d14da7 100644 --- a/tests/submodules/defaults.nix +++ b/tests/submodules/defaults.nix @@ -9,23 +9,27 @@ let instance4 = config.submodules.instances.instance4; instance5 = config.submodules.instances.instance5; - module = {name, ...}: { - options.args.value = mkOption { - description = "Submodule value"; - type = types.str; - }; + submodule = {name, ...}: { + imports = [ kubenix.modules.submodule ]; - options.args.defaultValue = mkOption { - description = "Submodule default value"; - type = types.str; + options = { + args.value = mkOption { + description = "Submodule value"; + type = types.str; + }; + + args.defaultValue = mkOption { + description = "Submodule default value"; + type = types.str; + }; }; }; in { imports = with kubenix.modules; [ test submodules ]; test = { - name = "submodules-defatuls"; - description = "Simple k8s submodule test"; + name = "submodules-defaults"; + description = "Simple submodule test"; assertions = [{ message = "should apply defaults by tag1"; assertion = instance1.config.args.value == "value1"; @@ -53,34 +57,34 @@ in { }; submodules.imports = [{ - modules = [module { + modules = [submodule { submodule = { name = "submodule1"; tags = ["tag1"]; }; }]; } { - modules = [module { + modules = [submodule { submodule = { name = "submodule2"; tags = ["tag2"]; }; }]; } { - modules = [module { + modules = [submodule { submodule = { name = "submodule3"; tags = ["tag2"]; }; }]; } { - modules = [module { + modules = [submodule { submodule = { name = "submodule4"; }; }]; } { - modules = [module { + modules = [submodule { submodule = { name = "submodule5"; }; diff --git a/tests/submodules/simple.nix b/tests/submodules/simple.nix index c8b3c05..1454f3b 100644 --- a/tests/submodules/simple.nix +++ b/tests/submodules/simple.nix @@ -25,20 +25,30 @@ in { } { message = "Should have submodule name set"; assertion = cfg.config.args.name == "instance"; + } { + message = "should have tag set"; + assertion = elem "tag" (cfg.config.submodule.tags); }]; }; + submodules.propagate.enable = true; submodules.imports = [{ module = {name, ...}: { - config.submodule.name = "submodule"; - options.args.value = mkOption { - description = "Submodule argument"; - type = types.str; + imports = [ kubenix.modules.submodule ]; + config = { + submodule.name = "submodule"; + submodule.tags = ["tag"]; }; - options.args.name = mkOption { - description = "Submodule name"; - type = types.str; - default = name; + options = { + args.value = mkOption { + description = "Submodule argument"; + type = types.str; + }; + args.name = mkOption { + description = "Submodule name"; + type = types.str; + default = name; + }; }; }; }]; diff --git a/tests/submodules/versioning.nix b/tests/submodules/versioning.nix index e316b5f..d876360 100644 --- a/tests/submodules/versioning.nix +++ b/tests/submodules/versioning.nix @@ -8,12 +8,14 @@ let inst-latest = config.submodules.instances.inst-latest.config; submodule = { - config.submodule.name = "subm"; + imports = [ kubenix.modules.submodule ]; options.version = mkOption { type = types.str; default = "undefined"; }; + + config.submodule.name = "subm"; }; in { imports = with kubenix.modules; [ test submodules ];