lib/modules: Introduce _module.checks.*.check

Previously the .enable option was used to encode the condition as well,
which lead to some oddness:
- In order to encode an assertion, one had to invert it
- To disable a check, one had to mkForce it

By introducing a separate .check option this is solved because:
- It can be used to encode assertions
- Disabling is done separately with .enable option, whose default can be
  overridden without a mkForce
This commit is contained in:
Silvan Mosberger 2020-11-30 22:38:56 +01:00
parent 991dfccbd1
commit 767d80099c
No known key found for this signature in database
GPG key ID: E8F1E9EAD284E17D
13 changed files with 56 additions and 48 deletions

View file

@ -155,17 +155,22 @@ rec {
default = {}; default = {};
internal = prefix != []; internal = prefix != [];
type = types.attrsOf (types.submodule { type = types.attrsOf (types.submodule {
# TODO: Rename to assertion? Or allow also setting assertion?
options.enable = mkOption { options.enable = mkOption {
description = '' description = ''
Whether to enable this check. Whether to enable this check. Set this to false to not trigger
<note><para> any errors or warning messages. This is useful for ignoring a
This is the inverse of asserting a condition: If a certain check in case it doesn't make sense in certain scenarios.
condition should be <literal>true</literal>, then this
option should be set to <literal>false</literal> when that
case occurs
</para></note>
''; '';
default = true;
type = types.bool;
};
options.check = mkOption {
description = ''
The condition that must succeed in order for this check to be
successful and not trigger a warning or error.
'';
readOnly = true;
type = types.bool; type = types.bool;
}; };
@ -189,9 +194,7 @@ rec {
and use <literal>''${options.path.to.option}</literal>. and use <literal>''${options.path.to.option}</literal>.
''; '';
type = types.str; type = types.str;
example = literalExample '' example = "Enabling both \${options.services.foo.enable} and \${options.services.bar.enable} is not possible.";
Enabling both ''${options.services.foo.enable} and ''${options.services.bar.enable} is not possible.
'';
}; };
}); });
}; };
@ -244,7 +247,7 @@ rec {
if lib.hasPrefix "_" name then value.message if lib.hasPrefix "_" name then value.message
else "[${showOption prefix}${optionalString (prefix != []) "/"}${name}] ${value.message}"; else "[${showOption prefix}${optionalString (prefix != []) "/"}${name}] ${value.message}";
in in
if ! value.enable then errors if value.enable -> value.check then errors
else if value.type == "warning" then lib.warn show errors else if value.type == "warning" then lib.warn show errors
else if value.type == "error" then errors ++ [ show ] else if value.type == "error" then errors ++ [ show ]
else abort "Unknown check type ${value.type}"; else abort "Unknown check type ${value.type}";
@ -885,8 +888,7 @@ rec {
}); });
config._module.checks = config._module.checks =
let opt = getAttrFromPath optionName options; in { let opt = getAttrFromPath optionName options; in {
${showOption optionName} = { ${showOption optionName} = lib.mkIf opt.isDefined {
enable = mkDefault opt.isDefined;
message = '' message = ''
The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it. The option definition `${showOption optionName}' in ${showFiles opt.files} no longer has any effect; please remove it.
${replacementInstructions} ${replacementInstructions}
@ -958,8 +960,7 @@ rec {
let val = getAttrFromPath f config; let val = getAttrFromPath f config;
opt = getAttrFromPath f options; opt = getAttrFromPath f options;
in { in {
${showOption f} = { ${showOption f} = lib.mkIf (val != "_mkMergedOptionModule") {
enable = mkDefault (val != "_mkMergedOptionModule");
type = "warning"; type = "warning";
message = "The option `${showOption f}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. Please read `${showOption to}' documentation and update your configuration accordingly."; message = "The option `${showOption f}' defined in ${showFiles opt.files} has been changed to `${showOption to}' that has a different type. Please read `${showOption to}' documentation and update your configuration accordingly.";
}; };
@ -1024,8 +1025,7 @@ rec {
}); });
config = mkMerge [ config = mkMerge [
{ {
_module.checks.${showOption from} = { _module.checks.${showOption from} = mkIf (warn && fromOpt.isDefined) {
enable = mkDefault (warn && fromOpt.isDefined);
type = "warning"; type = "warning";
message = "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'."; message = "The option `${showOption from}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption to}'.";
}; };

View file

@ -279,7 +279,8 @@ checkConfigOutput baz config.value.nested.bar.baz ./types-anything/mk-mods.nix
# Check that assertions are triggered by default for just evaluating config # Check that assertions are triggered by default for just evaluating config
checkConfigError 'Failed checks:\n- \[test\] Assertion failed' config ./assertions/simple.nix checkConfigError 'Failed checks:\n- \[test\] Assertion failed' config ./assertions/simple.nix
# Assertion is not triggered when enable is false # Assertion is not triggered when enable is false or condition is true
checkConfigOutput '{ }' config ./assertions/condition-true.nix
checkConfigOutput '{ }' config ./assertions/enable-false.nix checkConfigOutput '{ }' config ./assertions/enable-false.nix
# Warnings should be displayed on standard error # Warnings should be displayed on standard error

View file

@ -0,0 +1,8 @@
{
_module.checks.test = {
check = true;
message = "Assertion failed";
};
}

View file

@ -2,6 +2,7 @@
_module.checks.test = { _module.checks.test = {
enable = false; enable = false;
check = false;
message = "Assertion failed"; message = "Assertion failed";
}; };

View file

@ -2,20 +2,20 @@
_module.checks = { _module.checks = {
test1 = { test1 = {
enable = true; check = false;
message = "Assertion 1 failed"; message = "Assertion 1 failed";
}; };
test2 = { test2 = {
enable = true; check = false;
message = "Assertion 2 failed"; message = "Assertion 2 failed";
}; };
test3 = { test3 = {
enable = true; check = false;
message = "Warning 3 failed"; message = "Warning 3 failed";
type = "warning"; type = "warning";
}; };
test4 = { test4 = {
enable = true; check = false;
message = "Warning 4 failed"; message = "Warning 4 failed";
type = "warning"; type = "warning";
}; };

View file

@ -1,6 +1,6 @@
{ {
_module.checks.test = { _module.checks.test = {
enable = true; check = false;
message = "Assertion failed"; message = "Assertion failed";
}; };
} }

View file

@ -4,7 +4,7 @@
default = { bar.baz = {}; }; default = { bar.baz = {}; };
type = lib.types.attrsOf (lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (lib.types.attrsOf (lib.types.submodule {
_module.checks.test = { _module.checks.test = {
enable = true; check = false;
message = "Assertion failed"; message = "Assertion failed";
}; };
})); }));

View file

@ -4,7 +4,7 @@
default = { bar = {}; }; default = { bar = {}; };
type = lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (lib.types.submodule {
_module.checks.test = { _module.checks.test = {
enable = true; check = false;
message = "Assertion failed"; message = "Assertion failed";
}; };
}); });

View file

@ -4,7 +4,7 @@
default = {}; default = {};
type = lib.types.submodule { type = lib.types.submodule {
_module.checks.test = { _module.checks.test = {
enable = true; check = false;
message = "Assertion failed"; message = "Assertion failed";
}; };
}; };

View file

@ -1,7 +1,7 @@
{ {
_module.checks._test = { _module.checks._test = {
enable = true; check = false;
message = "Assertion failed"; message = "Assertion failed";
}; };

View file

@ -1,7 +1,7 @@
{ {
_module.checks.test = { _module.checks.test = {
enable = true; check = false;
type = "warning"; type = "warning";
message = "Warning message"; message = "Warning message";
}; };

View file

@ -25,28 +25,26 @@
<para> <para>
Checks can be defined using the <xref linkend="opt-_module.checks"/> option. Checks can be defined using the <xref linkend="opt-_module.checks"/> option.
Each check needs an attribute name, under which you have to define an enable Each check needs an attribute name, under which you can define a trigger
condition using <xref linkend="opt-_module.checks._name_.enable"/> and a assertion using <xref linkend="opt-_module.checks._name_.check"/> and a
message using <xref linkend="opt-_module.checks._name_.message"/>. Note that message using <xref linkend="opt-_module.checks._name_.message"/>.
the enable condition is <emphasis>inverse</emphasis> of what an assertion For the message, you can add
would be: To assert a value being true, the enable condition should be false
in that case, so that it isn't triggered. For the check message, you can add
<literal>options</literal> to the module arguments and use <literal>options</literal> to the module arguments and use
<literal>${options.path.to.option}</literal> to print a context-aware string <literal>${options.path.to.option}</literal> to print a context-aware string
representation of the option path. Here is an example showing how this can be representation of an option path. Here is an example showing how this can be
done. done.
</para> </para>
<programlisting> <programlisting>
{ config, options, ... }: { { config, options, ... }: {
_module.checks.gpgSshAgent = { _module.checks.gpgSshAgent = {
enable = config.programs.gnupg.agent.enableSSHSupport &amp;&amp; config.programs.ssh.startAgent; check = config.programs.gnupg.agent.enableSSHSupport -> !config.programs.ssh.startAgent;
message = "You can't enable both ${options.programs.ssh.startAgent}" message = "If you have ${options.programs.gnupg.agent.enableSSHSupport} enabled,"
+ " and ${options.programs.gnupg.agent.enableSSHSupport}!"; + " you can't enable ${options.programs.ssh.startAgent} as well!";
}; };
_module.checks.grafanaPassword = { _module.checks.grafanaPassword = {
enable = config.services.grafana.database.password != ""; check = config.services.grafana.database.password == "";
message = "The grafana password defined with ${options.services.grafana.database.password}" message = "The grafana password defined with ${options.services.grafana.database.password}"
+ " will be stored as plaintext in the Nix store!"; + " will be stored as plaintext in the Nix store!";
# This is a non-fatal warning # This is a non-fatal warning
@ -74,8 +72,8 @@
trace: warning: [grafanaPassword] The grafana password defined with trace: warning: [grafanaPassword] The grafana password defined with
services.grafana.database.password will be stored as plaintext in the Nix store! services.grafana.database.password will be stored as plaintext in the Nix store!
error: Failed checks: error: Failed checks:
- [gpgSshAgent] You can't enable both programs.ssh.startAgent and - [gpgSshAgent] If you have programs.gnupg.agent.enableSSHSupport
programs.gnupg.agent.enableSSHSupport! enabled, you can't enable programs.ssh.startAgent as well!
</programlisting> </programlisting>
<para> <para>
@ -87,12 +85,12 @@ error: Failed checks:
</para> </para>
<programlisting> <programlisting>
{ lib, ... }: { {
# Change the error into a non-fatal warning # Change the error into a non-fatal warning
_module.checks.gpgSshAgent.type = "warning"; _module.checks.gpgSshAgent.type = "warning";
# We don't care about this warning, disable it # We don't care about this warning, disable it
_module.checks.grafanaPassword.enable = lib.mkForce false; _module.checks.grafanaPassword.enable = false;
} }
</programlisting> </programlisting>
@ -113,7 +111,7 @@ error: Failed checks:
options.port = lib.mkOption {}; options.port = lib.mkOption {};
config._module.checks.portConflict = { config._module.checks.portConflict = {
enable = config.port == 80; check = config.port != 80;
message = "Port ${toString config.port} defined using" message = "Port ${toString config.port} defined using"
+ " ${options.port} is usually used for HTTP"; + " ${options.port} is usually used for HTTP";
type = "warning"; type = "warning";
@ -143,8 +141,8 @@ trace: warning: [myServices.foo/portConflict] Port 80 defined using
</para> </para>
<programlisting> <programlisting>
{ lib, ... }: { {
myServices.foo._module.checks.portConflict.enable = lib.mkForce false; myServices.foo._module.checks.portConflict.enable = false;
} }
</programlisting> </programlisting>

View file

@ -36,7 +36,7 @@ with lib;
name = "_${toString n}"; name = "_${toString n}";
isWarning = lib.isString value; isWarning = lib.isString value;
result = { result = {
enable = if isWarning then true else ! value.assertion; check = if isWarning then false else value.assertion;
type = if isWarning then "warning" else "error"; type = if isWarning then "warning" else "error";
message = if isWarning then value else value.message; message = if isWarning then value else value.message;
}; };