In nixpkgs tests, why might `services.userborn.enable = true` suppress `services.openssh.enable = true`?

I am writing a nixpkgs test for a NixOS PR I have in development, and I’ve tripped over something bizarre. Here’s a cut-down nixpkgs test that should reproduce the problem:

{ pkgs, ... }:
let
  makeTestVM = mutableUsers: extraOptions: {
    services.openssh.enable = true;
    users.mutableUsers = mutableUsers;
  } // extraOptions;
in
{
  name = "ssh-vs-userborn";

  nodes = {
    mutable-legacy = makeTestVM true {};
    immutable-legacy = makeTestVM false {};
    mutable-userborn = makeTestVM true { services.userborn.enable = true; };
    immutable-userborn = makeTestVM false { services.userborn.enable = true; };
  };

  testScript = ''
    for m in machines:
      with subtest(m.name):
        m.start()
        m.wait_for_unit("sshd.service")
  '';
}

(The full test actually creates some users and checks that certain of their properties are the same for userborn and the legacy account creation script.)

The intention here is that the test VMs should all be configured identically, except for the values of users.mutableUsers and services.userborn.enable. What actually happens is that the -userborn variant VMs don’t have the ssh daemon enabled at all – neither /etc/systemd/system/sshd.service nor /etc/systemd/system/multi-user.target.wants/sshd.service exists – and so the m.wait_for_unit() call fails for those VMs. Why is this happening?

1 Like

Found the bug. // does not merge deeply, so the userborn services block completely replaced the services.openssh.enable = true;. IDK if you can use mkMerge in nodes (you probably can), but try that instead of //.

1 Like

lib.attrsets.recursiveMerge seems to be the thing I wanted. Thanks.

1 Like