NixOS / nixpkgs

Nix Packages collection & NixOS
MIT License
17.66k stars 13.81k forks source link

Don't copy nginx/httpd vhost options to service modules using nginx #313412

Open Ma27 opened 4 months ago

Ma27 commented 4 months ago

Describe the bug

For instance services.jirafeau and services.dolibarr expose all of these options below services.X.nginx. There are a lot more cases that are trivial to find.

I think this is a bad practice and has the following downsides:

Since I have an nginx bias, all of the above is written with focus on nginx. The same applies to modules exposing httpd's options, of course.

Expected "behavior"

IMHO this is a symptom of not understanding how merging inside the module system works (i.e. you can just define the relevant portions in your service.nix). Given the issues associated with this I'd suggest to

cc @infinisil @roberth @aanderse @Guekka @NetaliDev @vtuan10 @melvyn2 @RaitoBezarius for opinions.


Add a :+1: reaction to issues you find important.

Guekka commented 4 months ago

I agree with this proposal. The current approach of exposing numerous options under services.X.nginx can indeed be confusing, especially for new users. I have experienced this confusion myself when searching for options while discovering NixOS.

However, we would need strict rules to get a clear documentation. Otherwise, we could end up with users writing a hardcoded virtualHosts:

services.nextcloud.enable = true;
services.nginx.virtualHosts.nextcloud = {}; // ...

This would obviously break if upstream changed the name of the virtual host.


On a broader scale, this pattern can be found very often within nixpkgs. Be it with nginx, redis, postgres... Each module can enable them and wrap their options or not.

Modules have no strict rules about what they can do or not. Why do some of them enable nginx/postgres/... for the user, whereas others require the user to enable these services themself? Usually, this comes down to the module requiring a specific config, but this is not always the case.

Ultimately, I'm wondering if we should require (through automation?) the documentation of each module to mention the other modules it configures. Something like

The nextcloud module configures nginx and redis. To modify default settings, use services.nginx.virtualHosts.${config.services.nextcloud.hostName} and services.redis.servers.${config.services.nextcloud.hostName}.


PS: out of curiosity @Ma27, any reason for pinging me, or did you just pick a random maintainer?

roberth commented 4 months ago
  • every option increases the evaluation time

Not wrong as a general rule of thumb, but it does not apply for options in un-evaluated submodules, and also not when _module.check = false. We might not want the latter, so here's a counterexample showing the prior.

--- a/nixos/modules/services/web-apps/jirafeau.nix
+++ b/nixos/modules/services/web-apps/jirafeau.nix
@@ -79,7 +79,7 @@ in
     };

     nginxConfig = mkOption {
-      type = types.submodule
+      type = throw "" types.submodule
         (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
       default = {};
       example = literalExpression ''

The above does break documentation, whose evaluation is cached anyway, so we can ignore that for the purpose of the example.

nix-repl> :b (nixos { fileSystems."/".device = "x"; boot.loader.grub.enable = false; documentation.enable = false; }).toplevel
...
This derivation produced the following outputs:
...

I could even build with throw "" mkOption there, but I don't want to give a false impression; don't have a good rule of thumb for when that works. Option tree handling is not optimal yet; see https://github.com/NixOS/nixpkgs/issues/291536.


Regardless, this pattern is wrong for a different reason. Option types are not guaranteed to be idempotent when their computed option values are reinterpreted as definitions (and then resolved and merged for the final nginx config). In fact it's easy to find an example where it goes wrong: all properties are lost, such as mkForce, mkBefore, etc. The way to preserve all that and only merge once is to use types.deferredModule (sorry for the bad fragment; we should stop using definition lists imo), or such types as attrsOf (deferredModule), if needed.

This solves the semantic issue, and gets rid of the duplicated option docs.

(OT: you could recover them with deferredModuleWith { staticModules } if you ever needed to, and if it's ok to import that set of modules)


  • some of the options don't even make any sense

Impossible to prevent in general, but absolutely agreed for docs. deferredModule will only render e.g. nginxConfig, but no sub-options.

It does make sense to have little convenience options for common modifications, although that could become a bit too arbitrary and bloated too. Maintainers will have to strike a balance.

aanderse commented 4 months ago

sounds good @Ma27

i especially agree strongly with the comments about documentation!

thanks for making this issue

Ma27 commented 4 months ago

PS: out of curiosity @Ma27, any reason for pinging me, or did you just pick a random maintainer?

I picked most of the people I could trace back with git blame who seemed to originally add this. I wanted to make sure that I'm not missing reasons to leave it that way.

However, we would need strict rules to get a clear documentation. Otherwise, we could end up with users writing a hardcoded virtualHosts:

Isn't the key for a virtualHost usually the domain it's served under? My suggestion is to instruct people to do something like services.nginx.virtualHosts.${config.services.nextcloud.hostName} = { ... };. That way you won't need to worry about that.

Be it with nginx, redis, postgres... Each module can enable them and wrap their options or not.

Sure, modules can touch other modules and to a certain degree it's a good thing. For instance, for PHP applications you need some kind of reverse proxy that serves assets and forwards the correct prefixes to FPM, so it makes sense to configure it[1]. It usually makes sense to wrap this behind some kind of services.foo.postgresql.enable option to allow people to build environments where reverse-proxy/database/whatever lives on a different node.

That's not my point though: some kind of wrapping tailored to the service is OK. It's not OK though to expose every single option of a different service module (as discussed above). Sooner or later you'll need to touch these modules directly.

The nextcloud module configures nginx and redis. To modify default settings, use services.nginx.virtualHosts.${config.services.nextcloud.hostName} and services.redis.servers.${config.services.nextcloud.hostName}.

I think we'll need way more metadata fo rthi sto work. Because at the end of the day, nginx & redis touch other modules (i.e. systemd.services), so it needs to be defined what's actually interesting. For now, I think it's OK to expose e.g. a hostName option and document in its description how the reverse-proxy can be modified.

Option types are not guaranteed to be idempotent when their computed option values are reinterpreted as definitions (and then resolved and merged for the final nginx config). In fact it's easy to find an example where it goes wrong: all properties are lost, such as mkForce, mkBefore, etc

Good point! I think I've seen an issue related to that already, but forgot to mention it originally. Thanks for adding this!

This solves the semantic issue, and gets rid of the duplicated option docs.

I'd argue that this approach is not desirable either:

It does make sense to have little convenience options for common modifications, although that could become a bit too arbitrary and bloated too

Agreed. Some basics (in the case discussed here e.g. enable, hostName & max "upload" size may make sense) are good to have. For everything else I'd argue it's better to "forward" users to the relevant modules (i.e. nginx in this case).

[1] I should add that I have opinions about the nginx case in particular nonetheless: https://github.com/NixOS/nixpkgs/issues/277723