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 1 commit
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
191 changes: 191 additions & 0 deletions rfcs/0042-config-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
---
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)
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;

# Use port specified in config or the upstream default otherwise
# Needed to open the correct port
port = cfg.config.port or 2546;
infinisil marked this conversation as resolved.
Show resolved Hide resolved

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"\>
infinisil marked this conversation as resolved.
Show resolved Hide resolved
for documentation on the supported values.
'';
};
};

config = mkIf cfg.enable {

# Set minimal config to get service working by default
services.foo.config = {
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?

};

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

networking.firewall.allowedTCPPorts = [ port ];
# ...
};
}
```

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 with `mkForce`
infinisil marked this conversation as resolved.
Show resolved Hide resolved

*: The only problem it doesn't solve is the coupling to upstream defaults for options that need to be known at evaluation time, such as `port` in the example above, but that's not really avoidable.

### 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.config`. These should make sure that the resulting text is somewhat properly formatted with readable indentation. 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:
infinisil marked this conversation as resolved.
Show resolved Hide resolved
- 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 programs 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.
infinisil marked this conversation as resolved.
Show resolved Hide resolved

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:
- Types of configuration settings can't be checked*, which can lead to packages failing at runtime instead of evaluation time. If the program supports a mode for checking its configuration, this problem can be solved elegantly by using it.
infinisil marked this conversation as resolved.
Show resolved Hide resolved
- 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.

*: It would still be possible to check them by using `types.addCheck` and adjusting the types description, but that sounds more painful than it's worth.

# Alternatives
[alternatives]: #alternatives

See [Motivation](#motivation)

# Unresolved questions
[unresolved]: #unresolved-questions

# Future work
[future]: #future-work