Flakes, packages and system users

I would like to write a flake that…

  • creates a user that has sudo access
  • that adds something to the sshd_config (and ensures it’s running)
  • adds a bash script (that the sshd_config references

and TBH I am totally lost where to even start.

Most flakes I found either are about installing a full system or building a native package.
I guess this might be closer to building a package?

Can anyone recommend an flake does maybe does something similar (so I can have a look)?
Or a link where I can read up on such things? The docs in the wiki seem fairly limited.

And I was also wondering what happens if two separate flakes would want to create a user with the same name - what would happen? Since that’s on the system level and not just in /nix/store

What I got so far:

flake.nix

{
  description = "my package";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
        myPackage = self.packages.${system}.myPackage;
      in {
        packages.myPackage = pkgs.callPackage ./myPackage { };
        config = {
          environment.systemPackages = [ myPackage ];
        };
      }
    );
}

configuration.nix

{ config, pkgs, ... }:
{
  nix = {
    packageOverrides = pkgs: rec {
      myPackage = pkgs.myPackage.overrideAttrs (oldAttrs: {
        preInstall = ''
          mkdir -p $out/etc/foo
        '';
      });
    };
  };

  users.extraUsers.deploy = {
    isNormalUser = true;
    createHome = true;
    home = "/home/foo";
    description = "foo";
    extraGroups = ["wheel"];
  };

  security.sudo.wheelNeedsPassword = false;
  security.sudo.extraConfig = ''
    foo ALL=(ALL:ALL) ALL
  '';

}

and the default.nix for the package

{ lib, buildInputs, ... }:

lib.package {
  pname = "myPackage";
  version = "1.0";
  src = ./.;
  isLibrary = false;

  meta = with lib; {
    description = "My custom package";
    license = licenses.mit;
  };
}

Now it seems like this should build the package for every eachDefaultSystem but it fails with

does not provide attribute **'packages.aarch64-darwin.default' or 'defaultPackage.aarch64-darwin'**

when trying nix build on darwin (just for testing - the final target will be linux).

And I am also not sure how apply the changes from configuration.nix.

assuming your target is a nixos system you want to add a nixos module output to your flake which is a way of adding configuration to a nixos system.

outputs = _: {
  nixosModules.myconfig = { config, pkgs, lib, ... }: {
    users.users.foo = {
      isNormal = true;
    };
  };

  security.sudo...

  services.openssh.extraConfig = ''
    # something here....
  '';
}

does this help? feel free to post very specific questions if you think it will help

2 Likes

Note that flakes are “just” a standardised way of sharing Nix packages or NixOS modules.

What you actually need to do is craft this NixOS module which could be done entirely independently of the flake and then expose this module via your flake.

1 Like

I tried this

  outputs = _: {
    nixosModules.myconfig = { config, nixpkgs, ... }: {
      users.users.foo = {
        isNormal = true;
      };
    };
  };

but it does not provide attribute **'packages.aarch64-darwin.default' or 'defaultPackage.aarch64-darwin'** - which is true.

And I don’t understand _: { yet.
Also I sometimes see pkg and sometimes nixpkgs. Is that just a variable name? or does it matter?
And with users.users.foo - why is it users.users?

I am basically trying to build a nix package and include it my other flake that builds my nixpkgs.lib.nixosSystems.

It should provide some special configurations. I guess I could also try this with a function - but it would be nice to have this available separately (for easier re-use).

OK. users.users is apparently “normal” and it got me further.
At least nix flake show does not show a problem (=evaluates?) and nix build is probably not the right thing to use because there is nothing to build?

  outputs = { self, nixpkgs }: {
    nixosModules = {
      myconfig = {
        users.users.foo = {
          isNormal = true;
        };

        security.sudo = {
          enable = true;
          extraRules = [{
            commands = [
              {
                command = "FIXME path to script from within the flake";
                options = [ "NOPASSWD" ];
              }
            ];
            groups = [ "wheel" ]; # FIXME only the user "foo"
          }];
        };

      };
    };
  };

I still need to create/copy a script via flake and restrict the sudo to the user (or maybe create a special group).

And I guess I can then use it like this:

{
  description = "my servers";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    myfoo.url = "github:tcurdt/myfoo";
  };

  outputs =
    { self
    , nixpkgs
    , ...
    } @ inputs: {

    nixosConfigurations.myserver = nixpkgs.lib.nixosSystem {

      modules = [
        ./hardware.nix
        myfoo.myconfig
      ];

    };

  };
}

What I tried:

  outputs = _: {
    nixosModules.myconfig = { self, nixpkgs }: {
      users.users.foo = {
        isNormal = true;
      };

I figured out that “_” is an empty set but I don’t understand the why. So I also tried:

  outputs = { self, nixpkgs }: {
    nixosModules = {
      myconfig = {
        users.users.foo = {
          isNormal = true;
        };

But when I include the flake

  modules = [
    ../hardware/foo.nix
    myfoo.nixosModules

or like this

  modules = [
    ../hardware/foo.nix
    myfoo.nixosModules.myconfig

I am getting “error: The option `myconfig’ does not exist” and I don’t quite understand why.

That said I am not even sure why one would want ‘myconfig’.

Why not just

  outputs = { self, nixpkgs }: {
    nixosModules = {
      users.users.foo = {
        isNormal = true;
      };

or even

  outputs = { self, nixpkgs }: {
    users.users.foo = {
      isNormal = true;
    };

I’m just passing by quickly, but I can answer your last questions.

You can spit out whatever outputs you want, so you could put either of those two final snippets. But they wouldn’t work, because they don’t match the conventional outputs that NixOS looks for. By convention, modules go under nixosModules–and, since a single flake can have many modules, it’ll be looking for named modules. That makes the last snippet ‘wrong’ (because NixOS will ignore the user attribute, you’ll just get a warning about “unrecognized output ‘users’”) and the second-from-last ‘wrong’ because user isn’t a valid module, since it’s not a function.

It’s necessary to have a conventional place to store modules, because there are other types of output, for example your nixosConfigurations.myserver output (or overlays, or shells, …). Without putting it in the right place, Nix doesn’t know if it’s supposed to be a module, a system config, or what.

myconfig is just a name. It can be whatever you want. So a valid nixosModules entry might be:

  outputs = { self, nixpkgs, ... }: {
    nixosModules = {    // <---- NixOS will, by convention, be looking under here for modules
      mySuperUserModule = { ... }: {   // <-- This is a function capable of accepting `config`, `lib`, etc--see below--so it's a valid module
        config = {
          users.users = {
            // ... etc ...
          };
          // ... etc ...
        };
      };
    };
  };

I think maybe you’re getting that error because your myconfig module isn’t a function. A module has to accept a bunch of arguments (config, lib, pkgs, and some less commonly-used ones) to be valid.

There’s a few different ways to specify the arguments that the module will accept:

  mymodule = _: { ... <insert config here> .... };

Take one argument and ignore it.

  mymodule = args: { ... <insert config here> ... };

Take one argument and name it args…you’ll find that args.config, args.lib, and args.pkgs all exist (because you’re being passed a single attrset, basically a hashtable, with those attributes).

  mymodule = { config, lib, pkgs }: { ... <insert config here> ... };

Take a single attrset argument, which must have config, lib, and pkgs attributes, and nothing else (for which reason, I think this wouldn’t actually work–there are probably other args).

  mymodule = { config, ... }: { ... <insert config here> ... };

Take a single attrset argument which must have config, and can have whatever other attributes the caller feels like passing (which we’re just going to ignore). If you need to use packages, you could use { config, pkgs, ... }: instead, for example.

So one way or another, your module must be a function, and it must be able to take a bunch of arguments (including those three I keep repeating), whether you intend to use them or not.

One final note: you may notice I stuck an extra config = { ... } in your module definition. That wasn’t strictly necessary: if you skip it, Nix will usually figure out (using a hack of some kind) what you meant and treat the top-level like config. Later, though, when you start using options and imports, it’ll get confusing… I think it’s simpler to just say that a module is a function from { config, pkgs, lib, ...} and to { config, ... }.

1 Like

Thank you so much for taking the time, @niten

It took me a while to figure this out - but it seems like I am on the right path.

What still feels a little fuzzy to me is something like this:

{ nixpkgs, sshhook, ... }:
name:
{
  hardware,
  hostPlatform,
  hostName,
}: nixpkgs.lib.nixosSystem {
...

The trinity of parameters, name, parameters does not quite compute for me yet.
A single set of inputs/parameters to a function - sure. But what’s the idea with these 3?

That’s really just a function taking three parameters, two of which happen to be attrsets, which are being pulled apart for their contents.

First thing to note is the syntax for arguments. A very simple Nix function looks like this:

myAdder = a: b: a + b;
myAdder 3 5; # 8

That function is taking two arguments, a and b, and adding them together. It’s equivalent to def myAddr(a, b): return a + b in Python.

Knowing that, you could rewrite the function you posted like this:

my_function = inputs: name: params: let
  nixpkgs = inputs.nixpkgs;
  sshhook = inputs.sshhook;

  hardware = params.hardware;
  hostPlatform = params.hostPlatform;
  hostName = params.hostName;

in nixpkgs.lib.nixosSystem { ... }

That would be effectively identical. But it’s ugly, right? We had to name these two arguments, inputs and params, even though we didn’t really care about them–we only cared about some of the attrs they contain. And then we have to manually pull out the attrs one at a time, and assign them to a variable.

So Nix has this syntactic sugar to help you rip open arguments that happen to be attrsets and directly use the attrs they contain as variables. So:

myAdder = coord: coord.x + coord.y;

Versus:

myAdder = { x, y }: x + y;

Again, those two are identical. You can call either one with: myAdder { x = 3; y = 5; }. But one is a lot cleaner-looking. It’s just saying “Expect a single attrset argument, containing an x and a y attribute, and assign those to x and y variables for me”.

Incidentally, myAdder { x = 3 } will give an error: missing expected argument y. And while I’m at it, myAdder { x = 3; y = 5; z = 7; } will also give an error: called with unexpected argument z! We can quickly fix that second error:

myAdder = { x, y, ... }: x + y;

Now you can call it with an extra z attribute, and it’ll just ignore it (because of the ...).

One last idea that’s worth learning early is currying. I said myAdder = x: y: ... was equivalent to def myAdder(x, y) in Python (or similar, C-like languages), but there’s an important difference.

In Python, this is an error: myAdder(3). That’s not enough arguments, the function was expecting 2 of them!

But in Nix, myAdder 3; is not an error. Instead, it returns a function expecting one more argument. So:

myAdder = x: y: x + y;
myAddTo3 = myAdder 3;

myAddTo3 5; # returns 8
myAddTo3 9; # returns 12

Basically, myAdder 3 sets x to 3, and then returns a function which still needs a y. You can call that new function with only one argument.

This is very useful for a config language. In your example, when you first load the flake you’ll have the inputs, nixpkgs and sshhook, but you won’t necessarily know the hostName or hardware yet. So you can just supply the first argument, and pass the function on to someone who will have those things.

I know that’s kinda weird (and may seem to conflict with the attrset thing above, you can’t supply only some of the required attrs in an attrset argument), but it’s something you should keep in mind for when you’re looking at a function call and thinking “wait, that’s not enough arguments…”

1 Like

You probably couldn’t hear the click that just happened in my brain.
Now that explains a lot.

Well, … it would make a hell lot more accessible and readable IMO :slight_smile:
But certainly a bit more verbose.

My first guess is that in one case it is a specific attribute set type and in the other case one it is parameters to a function. The parameters allow the currying - but the attribute set type is a type.
Is that the right way of seeing it?

Yep, that’s right. Currying only works for arguments, not for the contents of arguments.

Glad to hear it helped, haha. I remember a lot of this being painful to learn…

1 Like