Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC 0042] NixOS settings options #42

Merged
merged 44 commits into from
Jan 14, 2021
Merged
Changes from 2 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
4c436f1
config-option: initial summary and motivation
infinisil Mar 10, 2019
68f1b44
fixup! Address comments, add Defaults and Configuration Checking sect…
infinisil Mar 24, 2019
feb2e40
fixup! Missing semicolon
infinisil Mar 24, 2019
3d072be
fixup! Add TODO note
infinisil Mar 24, 2019
3e60aab
fixup! Address review, missing semicolon
infinisil Mar 24, 2019
cde38bc
fixup! Add better manual defaults note to future work
infinisil Mar 24, 2019
9a263ef
fixup! Add another referenc to a relevant PR
infinisil Mar 26, 2019
88b2997
fixup! Add some todos and fix a sentence in the summary
infinisil Mar 27, 2019
9986adf
fixup! Add another relevant PR
infinisil Mar 27, 2019
d59771b
Add section on backwards compatibility for config settings
infinisil Apr 3, 2019
1231eaa
Expand section on configuration file format limitations
infinisil Apr 3, 2019
9e37c0e
Note that popular/main settings also qualify for having their own Nix…
infinisil Apr 3, 2019
166d8d4
Move sections around to make more sense
infinisil Apr 3, 2019
1e0bdd6
Add limitation for backwards compatibility with existing modules
infinisil Apr 3, 2019
7dcc00b
Use roundcube as representable unrepresentable config format
infinisil Apr 3, 2019
dd9e628
English fix: this -> then
infinisil Apr 4, 2019
e9082a9
Reword RFC to be more inline with more additional options
infinisil Apr 23, 2019
0e832f3
Add another previous implementation
infinisil Apr 29, 2019
d1a8974
Add TODO for option.*.files for config checks
infinisil Jun 30, 2019
ada0658
Add shepherd leader/team
infinisil Jul 18, 2019
10d8b15
Rewrite: Summary and Motivation
infinisil Jul 19, 2019
18b3b4e
Links and stuff
infinisil Jul 19, 2019
39a4f80
Rewrite: Implementation part 1
infinisil Jul 19, 2019
0fb7758
Rewrite second part
infinisil Jul 24, 2019
b50d1a3
Better part 1 summary
infinisil Jul 24, 2019
3630c2c
Reformulate second part summary
infinisil Jul 24, 2019
abd729f
Move around a bit and extend future work
infinisil Jul 24, 2019
49a2898
Remove some temporary stuff
infinisil Jul 24, 2019
e1d248c
Add table for defaults
infinisil Jul 24, 2019
28a3111
Use table for valuable options
infinisil Jul 24, 2019
4f98924
Some adjustments
infinisil Jul 24, 2019
15a6c5d
Add link to the example from the summary
infinisil Jul 24, 2019
64aeeda
Move around and expand part 1 a bit
infinisil Jul 24, 2019
65842f1
Some more restructuring and tweaks
infinisil Jul 25, 2019
13d4fe8
Add cross links
infinisil Jul 25, 2019
aef7f5f
Merge branch 'structured-settings' into config-option
infinisil Jul 25, 2019
a48b55b
Add link to types.eithers PR
infinisil Aug 1, 2019
7d6353d
Mention how extraConfig can break services
infinisil Sep 6, 2019
2c63f95
Add link to lazyAttrsOf PR
infinisil Oct 12, 2019
5ab880f
Add format type implementation PR
infinisil Dec 12, 2019
e719102
Overhaul main parts
infinisil Jan 16, 2020
3361309
Update rfcs/0042-config-option.md
Mic92 Jul 30, 2020
9ef0221
Clean up
infinisil Dec 18, 2020
d46bafd
Remove fpletz from shepherd team
lheckemann Jan 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions rfcs/0042-config-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
---
feature: config-option
start-date: 2019-03-10
author: Silvan Mosberger
co-authors: (find a buddy later to help our with the RFC)
infinisil marked this conversation as resolved.
Show resolved Hide resolved
related-issues: (will contain links to implementation PRs)
---

# Summary
[summary]: #summary

A lot of NixOS options exist to specify single settings of configuration files. Along with such options come multiple disadvantages, such as having to synchronize with upstream changes, option bloat and more. An alternative is to provide options for passing the configuration as a string, such as the common `configFile` or `extraConfig` options, but this also comes with a set of disadvantages, including difficulty to override values and bad modularity.
infinisil marked this conversation as resolved.
Show resolved Hide resolved

This RFC proposes aims to solve these problems by encouraging NixOS module authors to provide an option for specifying the programs configuration file as a Nix value, providing a set of utility functions for writing these options conveniently, and updating the documentation to recommend this way of doing program configuration.

# Motivation
[motivation]: #motivation

NixOS commonly has 2 models of specifying configuration for programs, each with their own set of problems. This RFC aims to solve all of them.

## Single option for every setting

Having a single option for every setting in the configuration file, this often gets combined with an `extraConfig` option to provide greater flexibility. Problems:

- Coupling to upstream
- When upstream adds or removes settings, the NixOS module needs to be updated to reflect that.
- Upstream adds a setting: If the module has an `extraConfig` option people might set the new setting there. But if we ever add it as a NixOS option, we'll have trouble merging the values together with what the user already specified in `extraConfig`
- Upstream removes a setting (backwards incompatible): The NixOS module is straight up broken in nixpkgs until somebody fixes it, end users can't fix it themselves (unless the module provides a `configFile` option which can override the generated one). The same can also happen when the user uses overlays or `disabledModules` to cause a module/package version mismatch.
infinisil marked this conversation as resolved.
Show resolved Hide resolved
- The documentation of the upstream settings needs to be copied over to the NixOS options, which means it might get out of sync with changes
- The upstream defaults are being copied to the NixOS modules, so we need to also update the defaults whenever upstream changes them. This can be solved by using `nullOr` to allow for a `null` value to indicate that the upstream default shall be used, but that's not very nice.
infinisil marked this conversation as resolved.
Show resolved Hide resolved
- Option bloat: <s>NixOS evaluation time is still linear in the number of *available* options for all users, even though everybody only uses a fraction of them. This means that when a module adds 100 new options, this leads to a direct increase in evaluation time for every `nixos-rebuild switch` of everybody using NixOS.</s> With high confidence debunked in [#57477](https://github.com/NixOS/nixpkgs/issues/57477).
- Timeconsuming to implement, tedious to review and hard to maintain
- It takes a non-zero amount of time to write all the wanted options out into a NixOS module
- Reviewing is tedious because people need to make sure types are correct, descriptions are fitting, defaults are acceptable, for every option added. Due to the size and repetitiveness, people are also less willing to thoroughly review the code.
- The bigger the NixOS module is the harder it is to maintain, and the less people want to actually maintain it.
- Responsibility for backwards compatibility: By adding such options, we obligate ourselves to handle backwards incompatibilites on our side. We will have to support these options for a long time and can't remove them without at least one person being annoyed about it.
infinisil marked this conversation as resolved.
Show resolved Hide resolved

## `configFile` or `extraConfig` option

An option for specifying the contents of the configuration file directly with `configFile` or `extraConfig`. Problems:

- Not very modular at all
- In case of json or a `configFile` option, you can only assign the option once, merging is impossible
- In case of ini (or similar), assigning a single option multiple times to make use of list concatenation, ordering or priorities is impossible, so in general you can't e.g. override a default value set somewhere else.
- No syntax checking: Users will have to know the syntax of the configuration language and encode their values properly, any syntax error will only be realized when the program errors at runtime.

## Occurences of problems

- Because prometheus uses options to encode every possible setting, [#56017](https://github.com/NixOS/nixpkgs/pull/56017) is needed to allow users to set a part of the configuration that wasn't encoded yet.
- Because strongswan-ctl uses options to encode its full configuration, changes like [#49197](https://github.com/NixOS/nixpkgs/pull/49197) are needed to update our options with upstream changes.
- Pull requests like [#57036](https://github.com/NixOS/nixpkgs/pull/57036) or [#38324](https://github.com/NixOS/nixpkgs/pull/38324) are needed because users wish to have more configuration options than the ones provided.

## Previous discussions

- https://github.com/NixOS/nixpkgs/pull/44923#issuecomment-412393196
- https://github.com/NixOS/nixpkgs/pull/55957#issuecomment-464561483 -> https://github.com/NixOS/nixpkgs/pull/57716
infinisil marked this conversation as resolved.
Show resolved Hide resolved

## Implementations

This idea has been implemented already in some places:
- [#45470](https://github.com/NixOS/nixpkgs/pull/45470)
- [#52096](https://github.com/NixOS/nixpkgs/pull/52096)
- [My Murmur module](https://github.com/Infinisil/system/blob/45c3ea36651a2f4328c8a7474148f1c5ecb18e0a/config/new-modules/murmur.nix)

# Detailed design
[design]: #detailed-design

For specifying configuration files to programs in NixOS options, there should be a main single option called `config` which represents the configuration of the program as a Nix value, which can then be converted to the configuration file format the program expects. This may look as follows:

```nix
{ config, lib, ... }: with lib;
let

cfg = config.services.foo;

configText = configGen.json cfg.config;

in {

options.services.foo = {
enable = mkEnableOption "foo service";

config = mkOption {
infinisil marked this conversation as resolved.
Show resolved Hide resolved
type = lib.types.config.json;
default = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This should always be empty. All defaults that are specified here are forgotten when a configuration sets a setting.

This happens because specifying defaults has the effect of lib.mkOptionDefault (like lib.mkDefault) on the whole attribute set instead of individual settings.

Likewise, you could make the same mistake in the config.x.y.z.settings definition, assuming { a = lib.mkDefault 1; } would be equivalent to lib.mkDefault { a = 1; }, which it is not. Only the prior preserves a when another setting (not a) is defined by the user.

For a real life fix, see NixOS/nixpkgs#110614

@infinisil, perhaps we could make default = {} implied whenever freeformType is set? It'd be less tempting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roberth That reads to me like an important use case to think through thoroughly in nickel (in case it's not already a in depth first tree merge)

Copy link
Member

@roberth roberth Jan 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blaggacao then please take responsibility and post on that repo instead. Let's keep this PR on-topic, respecting the 40+ participants' time and attention. Thank you.


Update *: I regret the tone of my initial comment. I should have phrased this as advice instead. You may have read this as an accusation, which was not my intent.

Your comments are appreciated, even this one. However, I felt this case was exceptional for another reason, because of the large number of people subscribed to this thread and my judgement that the overlap between audiences was small, based on the fact that nickel is a separate, experimental configuration language whereas participants in this discussion are mostly interested in improving a proven configuration system they already use.

By all means, keep commenting. Just be aware of your audience when it's not a small thread.

*: I'm sending a PM about this edit; no worries.

For posterity, this comment had three +1s at the time of the update.

This comment was marked as resolved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been planning to make a PR for allowing something like

mkOption {
  recursiveDefault = {
    foo.bar = 10;
  };
}

Which would be equivalent to

{
  config.foo.bar = mkOptionDefault 10;
}

This would also allow rendering the default in the manual

description = ''
Configuration for foo. Refer to <link xlink:href="https://example.com/docs/foo"/>
for documentation on the supported values.
'';
};
};

config = mkIf cfg.enable {

# Set minimal config to get service working by default
services.foo.config = {
# We don't use mkDefault, as this module requires this value in order to function
dataPath = "/var/lib/foo";
logLevel = mkDefault "WARN";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought a little bit about this and I personally think that one would like to see some kind of mechanism that prints all default values into the summary of config. But IIRC only config options are evaluated when generating the manual, so I'm not even sure if that's possible without too much effort.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would certainly be nice in the future, I feel like it should be possible, by just determining the default by actually evaluating the options value with an empty configuration.nix, instead of just looking at the options default attribute.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea and would be something I'd probably help to implement. How about adding this to "Future work" for now? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is pretty important that the defaults in use should show up in the documentation. Perhaps the mkOption function could be adjusted to allow setting a priority of the default option? Then you could have something like

mkOption {
  default = {
    logLevel = "WARN";
    port = 2546;
  };
  defaultModifier = def: mapAttrs (n: mkOptionDefault) def;
  # …
}

To have user config merged into the option default value unless the user applies mkForce.

I haven't thought this through very carefully, though, so I might have missed some point causing this not to work.

But this may be an easier way to solve this documentation issue instead of attempting to evaluate an "empty" configuration. A difficulty, for example, is that we have to figure out how to actually get the config option to be populated. In the foo module, for example, you would have to know that the enable option should be set to true.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about something like

mkOption {
  default = lib.noPriority (mapAttrs (n: mkOptionDefault) {
    logLevel = "WARN";
    port = 2546;
  });
}

The manual could generate

default = {
  logLevel = <default> "WARN";
  port = <default> 2546;
}

But then there's also the problem with defaults that depend on other options, these can't be in the manual. And then the default declarations would have to be split up in the module itself.

And there's also the problem with serializing nix values from nix itself. We can't properly escape stuff, and multi-line strings won't work, and interpolated values, derivations.. I feel like we almost need some Nix language change to make this work properly.

And then there's also the discussion on which defaults should be seen by the user at all. Does it make sense that logType = "syslog" should be in the manual for the user to see? Probably not. See the Defaults section of this RFC as well, the first type of default specifically shouldn't be seen by the user.

I guess the simplest solution is to just say "A reasonable default base configuration is set in order for the module to work, see for details". We already link to the module files already anyways.

Not entirely sure what to do about this. But then again, having this problem is nothing inherent to this RFC, lots of modules already set defaults in this way invisible to users.

Copy link

@pacien pacien Jun 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any advantage in using mkDefault to override some configuration keys over using apply = recursiveUpdate default in mkOption like here?

      config = mkOption rec {
        type = types.attrs;
        apply = recursiveUpdate default;
        default = {
          ircService = {
            databaseUri = "nedb://${dataDir}/data";
            passwordEncryptionKeyPath = "${dataDir}/passkey.pem";
          };
      };

This has the advantage of already showing the default values in the documentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this apply way doesn't seem too bad, I don't think it's a good idea because it completely sidesteps the module system, which leads to some unexpected behavior. E.g. in your example if a user sets config.ircService = mkForce {}, this doesn't do anything, because recursiveUpdate doesn't know not to recurse over that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add addDefaultAnnotation = annotation: content: { _type = "annotation:default"; inherit content; }; and let the documentation builder use that for describing the value in the manual?

# Upstream default, needed for us to open the firewall
port = mkDefault 2546;
};

environment.etc."foo.json".text = configText;

networking.firewall.allowedTCPPorts = [ cfg.config.port ];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this example behind a cfg.enable is a very bad idea IMHO, because of NixOS/nixpkgs#19504 (comment).

# ...
};
}
```

This approach solves all* of the above mentioned problems. In addition we have the following properties that work with this approach out of the box:
infinisil marked this conversation as resolved.
Show resolved Hide resolved
- Ability to easily query arbitrary configuration values with `nix-instantiate --eval '<nixpkgs/nixos>' -A config.services.foo.config`
infinisil marked this conversation as resolved.
Show resolved Hide resolved
- The configuration file is well formatted with the right amount of indentation everywhere
infinisil marked this conversation as resolved.
Show resolved Hide resolved
- Usually hardcoded defaults can now be replaced by simple assignment of the `config` option, which also allows people to override those values. See the [Defaults](#defaults) section for more details.

*: The only problem it doesn't solve is the coupling to upstream defaults for required defaults, see the [Defaults](#defaults) section.

### Defaults

Depending on how default settings matter, we need to set them differently and for different reasons:
- If the module needs a specific value for a setting because of how the module or NixOS works (e.g. `logger = "systemd"`, because NixOS uses `systemd` for logging), then the value should *not* use `mkDefault`. This way a user can't easily override this setting (which would break the module in some way) and will have to use `mkForce` instead to change it. This also indicates that they are leaving supported territory, and will probably have to change something else to make it work again (e.g. if they set `logger = mkForce "/var/log/foo"` they'll have to change their workflow of where to look for logs).
- If the program needs a setting to be present in the configuration file because otherwise it would fail at runtime and demand a value, the module should set this value *with* a `mkDefault` to the default upstream value, which will then be the equivalent of a starter configuration file. This allows users to easily change the value, but also enables a smooth first use of the module without having to manually set such defaults to get it to a working state. Optimally modules should Just Work (tm) by setting their `enable` option to true.
- If the module itself needs to know the value of a configuration setting at evaluation time in order to influence other options (e.g. opening the firewall for a services port), we may set upstream's default with a `mkDefault`, even though the program might start just fine without it. This allows the module to use the configuration setting directly without having to worry whether it is set at all at evaluation time.

If all of the above points don't apply to a configuration setting, that is the module doesn't care about the value, the program doesn't care about the setting being present and we don't need the value at evaluation time, there should be no need to specify any default value.
infinisil marked this conversation as resolved.
Show resolved Hide resolved

### Configuration checking

One general downside of this approach is that the module system is not able to check the types and values of the configuration file, which would be fast, simple and give good error messages by default. While it would be possible to use `types.addCheck` for the type of the `config` option, this sounds more painful than it's worth and would lead to bad error messages, so we'll ignore this here. Here are some alternatives.

#### Configuration checking tools

A number of programs have tools for checking their configuration that don't need to start the program itself. We can use this to verify the configuration at **build time** by running the tool for a derivation build. While this is not as fast as if we had the module system do these checks, which would be at evaluation time already, it is faster than the program failing at runtime due to misconfiguration. These tools however are also more powerful than the module system and can integrate tightly with the program itself, allowing for more thorough checks. If a configuration checking tool is available, optimally by the program itself, it should be used if possible, as it can greatly improve user experience. The following illustrates an example of how this might look like
infinisil marked this conversation as resolved.
Show resolved Hide resolved

```nix
{ config, pkgs, lib, ... }: with lib;

let

configText = configGen.json cfg.config;

configFile = pkgs.runCommand "foo-config.json" {
# Because this program will be run at build time, we need `nativeBuildInputs` instead of `buildInputs`
nativeBuildInputs = [ pkgs.foo ];

inherit configText;
passAsFile = [ "configText" ];
} ''
foo check-config $configTextPath
cp $configTextPath $out
'';

in { /* ... */ }
```

#### Ad-hoc checks with assertions

While not as optimal as a configuration checker tool, assertions can be used to add flexible ad-hoc checks for type or other properties at **evaluation time**. It should only be used to ensure important properties that break the service in ways that are otherwise hard or slow to detect (and easy to detect for the module system), not for things that make the service fail to start anyways (unless there's a good reason for it). The following example only demonstrates how assertions can be used for checks, but any reasonable program should bail out early in such cases, which would make these assertions redundant, and only add more coupling to upstream, which we're trying to avoid.

```nix
{ config, lib, ... }: with lib; {
# ...
config = mkIf cfg.enable {
# Examples only for demonstration purposes, don't actually add assertions for such properties
assertions = [
{
assertion = cfg.config.enableLogging or true -> cfg.config ? logLevel;
message = "You also need to set `services.foo.config.logLevel` if `services.foo.config.enableLogging` is turned on.";
}
{
assertion = cfg.config ? port -> types.port.check cfg.config.port;
message = "${toString cfg.config.port} is not a valid port number for `services.foo.config.port`."
}
]
};
}
```

TODO: Are there any good examples of using assertions for configuration checks at all?

### Configuration types

A set of types for common configuration formats should be provided in `lib.types.config`. Such a type should encode what values can be set in files of this configuration format as a Nix value, with the module system being able to merge multiple values correctly. This is the part that checks whether the user set an encodeable value. This can be extended over time, but could include the following as a start:
- JSON
- YAML, which is probably the same as JSON
- INI
- A simple `key=value` format
- A recursive `key.subkey.subsubkey=value` format
infinisil marked this conversation as resolved.
Show resolved Hide resolved

Sometimes programs have their own configuration formats, in which case the type should be implemented in the program's module directly.

### Configuration format writers

To convert the Nix value into the configuration string, a set of configuration format writers should be provided under `lib.configGen`. These should make sure that the resulting text is somewhat properly formatted with readable indentation. Things like `builtins.toJSON` are therefore not optimal as it doesn't add any spacing for readability. These writers will have to include ones for all of the above-mentioned configuration types. As with the type, if the program has its own configuration format, the writer should be implemented in its module directly.

### Documentation

The nixpkgs manual should be updated to recommend this way of doing program configuration in modules, along with examples.

## Limitations

- Limited to configuration file formats representable conveniently in Nix, such as JSON, YAML, INI, key-value files, or similar formats. Examples of unsuitable configuration formats are Haskell, Lisp, Lua or other generic programming languages. For those it is recommended to not hardcode anything and provide a `config` option with type `types.str` or `types.lines` if it makes sense to merge multiple assignments of it.
infinisil marked this conversation as resolved.
Show resolved Hide resolved

## Additional config options
Sometimes it makes sense to have an additional NixOS option for a specific configuration setting. In general this should be discussed on a case-by-case basis to judge whether it makes sense. However keep in mind that it's always possible to add more options later on, but you can't as easily remove existing options. Instances of where it can make sense are:
- Settings that are necessary for the module to work and are different for every user, so they can't have a default. Examples are `services.dd-agent.api_key`, `services.davmail.url` or `services.hydra.hydraURL`. Having a separate option for these settings can give a much better error message when you don't set them (instead of failing at runtime or having to encode the requirement in an assertion) and better discoverability.
- Password settings: Some program's configuration files require passwords in them directly. Since we try to avoid having passwords in the Nix store, it is advisable to provide a `passwordFile` option as well, which would replace a placeholder password in the configuration file at runtime.

This `config` approach described here is very flexible for these kind of additions, here's an example where we add a `domain` setting to the above service as a separate NixOS option:
```nix
{ config, lib, ... }: with lib; {

options.services.foo = {
# ...

domain = mkOption {
type = types.str;
description = "Domain this service operates on.";
};
};

config = mkIf cfg.enable {
services.foo.config = {
# ...
domain = mkDefault cfg.domain;
};
};
}
```

Even though we have two ways of specifying this configuration setting now, the user is free to choose either way.

# Drawbacks
[drawbacks]: #drawbacks

There are some disadvantages to this approach:
- If there is no configuration checking tool as explained in [this section](#configuration-checking-tools), the types of configuration settings can't be checked as easily, which can lead to packages failing at runtime instead of evaluation time. Refer to [Configuration checking](#configuration-checking) for more info.
- Documentation for the configuration settings will not be available in the central NixOS manual, instead the upstream documentation has to be used, which can be unfamiliar and harder to read.

# Alternatives
[alternatives]: #alternatives

See [Motivation](#motivation)

# Unresolved questions
[unresolved]: #unresolved-questions

# Future work
[future]: #future-work