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

feat: support rootless containers via home-manager #3

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 74 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ Manages Podman containers and networks on NixOS via [Quadlet](https://docs.podma

Compared to alternatives like [`virtualisation.oci-containers`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/oci-containers.nix) or [`arion`](https://github.com/hercules-ci/arion), `quadlet-nix` is special in that:

| | `quadlet-nix` | `oci-containers` | `arion` |
| -------------------------------------------------------- | ------------- | ---------------- | ------- |
| **Supports networks** | ✅ | ❌ | ✅ |
| **Updates / deletes networks on change** | ✅ | / | ❌ |
| **Supports [podman-auto-update](podman-auto-update)** | ✅ | ✅ | ❌ |
| | `quadlet-nix` | `oci-containers` | `arion` |
|-------------------------------------------------------|---------------|------------------|---------|
| **Supports networks** | ✅ | ❌ | ✅ |
| **Updates / deletes networks on change** | ✅ | / | ❌ |
| **Supports [podman-auto-update][podman-auto-update]** | ✅ | ✅ | ❌ |
| **Supports rootless containers** | ✅ | ❌ | ❓ |

[podman-auto-update]: https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html

## How

## How (rootful)

See [`container.nix`](./container.nix) and [`network.nix`](./network.nix) for all options.

### `flake.nix`

Expand Down Expand Up @@ -55,4 +59,68 @@ Compared to alternatives like [`virtualisation.oci-containers`](https://github.c
}
```

## How (rootless)

See [`container.nix`](./container.nix) and [`network.nix`](./network.nix) for all options.

### `flake.nix`

```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager.url = "github:nix-community/home-manager";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
quadlet-nix.inputs.nixpkgs.follows = "nixpkgs";
quadlet-nix.inputs.home-manager.follows = "home-manager";
};
outputs = { nixpkgs, quadlet-nix, home-manager, ... }@attrs: {
nixosConfigurations.machine = nixpksg.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
SEIAROTg marked this conversation as resolved.
Show resolved Hide resolved
quadlet-nix.nixosModules.quadlet
];
};
};
}
```

### `configuration.nix`

```nix
{
# ...
SEIAROTg marked this conversation as resolved.
Show resolved Hide resolved
users.users.alice = {
# ... insert your user config here
# The follow lines are the important ones for rootless podman
home = "/home/alice";
linger = true;
autoSubUidGidRange = true;
};
home-manager.users.alice =
{ pkgs, config, ... }:
{
imports = [ inputs.quadlet-nix.homeManagerModules.default ];
home.stateVersion = "21.11";
home.homeDirectory = "/home/alice";
systemd.user.startServices = "sd-switch"; # This is crucial to ensure the systemd services are (re)started
virtualisation.user.quadlet.containers = {
echo-server = {
autoStart = true;
serviceConfig = {
RestartSec = "10";
Restart = "always";
};
containerConfig = {
image = "docker.io/mendhak/http-https-echo:31";
publishPorts = [ "127.0.0.1:8080:8080" ];
userns = "keep-id";
};
};
};
};
}
```
26 changes: 26 additions & 0 deletions container.nix
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,30 @@ let
property = "ContainerName";
};

dns = quadletUtils.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.55.1" ];
description = "--dns";
property = "DNS";
};

dnsSearch = quadletUtils.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "foo.com" ];
description = "--dns-search";
property = "DNSSearch";
};

dnsOption = quadletUtils.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "ndots:1" ];
description = "--dns-option";
property = "DNSOption";
};

dropCapabilities = quadletUtils.mkOption {
type = types.listOf types.str;
default = [ ];
Expand Down Expand Up @@ -431,6 +455,8 @@ let

serviceConfigDefault = {
Restart = "always";
# podman rootless requires "newuidmap" (the suid version, not the non-suid one from pkgs.shadow)
Environment = "PATH=/run/wrappers/bin";
TimeoutStartSec = 900;
};
in {
Expand Down
6 changes: 4 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
{
description = "NixOS module for Podman Quadlet";
description = "NixOS and home-manager module for Podman Quadlets";

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

outputs = { nixpkgs, ... }:
outputs = { self, nixpkgs, ... }:
let
libUtils = import "${nixpkgs}/nixos/lib/utils.nix";
in {
nixosModules.quadlet = import ./nixos-module.nix { inherit libUtils; };
homeManagerModules.quadlet = import ./home-manager-module.nix { inherit libUtils; };
homeManagerModules.default = self.homeManagerModules.quadlet;
};
}
99 changes: 99 additions & 0 deletions home-manager-module.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{ libUtils }:
{
config,
osConfig,
options,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.user.quadlet;
quadletUtils = import ./utils.nix {
inherit lib;
systemdLib = (libUtils { inherit lib config pkgs; }).systemdUtils.lib;
};
containerOpts = lib.types.submodule (import ./container.nix { inherit quadletUtils; });
networkOpts = lib.types.submodule (import ./network.nix { inherit quadletUtils pkgs; });
in
{
options.virtualisation.user.quadlet = {
Copy link
Owner

Choose a reason for hiding this comment

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

virtualisation.quadlet.user, as there isn't really the concept of "user virtualisation".

Copy link
Author

Choose a reason for hiding this comment

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

I followed the convention that exists in nixpkgs:

systemd.service -> systemd.user.services

But happy to change it.

Copy link
Owner

Choose a reason for hiding this comment

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

I think systend.user.services is based on that "user systemd" being something. We can have a service managed by "user systemd", or similarly a container managed by a concrete system named "user quadlet" / "user podman", rather than an abstract idea if "user virtualisation".

From the ownership perspective, the systemd module conceptually owns systemd.** options and does user vs system separation inside that scope, while quadlet-nix should probably do the same and implement the separation within its own land virtualisation.quadlet.**.

What do you think?

Copy link
Author

Choose a reason for hiding this comment

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

Yes you've convinced me. I'll make the change!

autoUpdate = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
calendar = lib.mkOption {
Copy link
Owner

Choose a reason for hiding this comment

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

Nice. Do you want to add the same timer for rootful quadlet?

Copy link
Author

Choose a reason for hiding this comment

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

Sure can do.

type = lib.types.str;
default = "*-*-* 00:00:00";
};
};
containers = lib.mkOption {
type = lib.types.attrsOf containerOpts;
default = { };
};
networks = lib.mkOption {
type = lib.types.attrsOf networkOpts;
default = { };
};
};
config =
let
allObjects = (lib.attrValues cfg.containers) ++ (lib.attrValues cfg.networks);
in
{
xdg.configFile =
let
links = pkgs.linkFarm "user-quadlet-service-symlinks" (
map (p: {
name = p._unitName;
path = "/run/user/${
toString osConfig.users.users.${config.home.username}.uid
}/systemd/generator/${p._unitName}";
}) allObjects
);
in
lib.mergeAttrsList (
map (p: {
# Install the .container, .network, etc files
"containers/systemd/${p._configName}" = {
text = p._configText;
};
# Link the corresponding .service files so that the home-manager activation process knows about them
"systemd/user/${p._unitName}" = {
source = "${links}/${p._unitName}";
Copy link
Owner

Choose a reason for hiding this comment

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

Is it possible to link directly from /run/user/... without linkFarm? With that we might even be able to use $UID without relying on uid being explicitly set in nixos config.

Copy link
Author

Choose a reason for hiding this comment

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

Yea, this bit I'm not 100% happy with either. The symlinks are definitely needed so that the home manager reactivation can restart them. But while home-manager let's us write files into $HOME, it doesn't let us write symlinks directly (as far as I can find). So I used linkFarm which creates a derivation with links from generated-service-name.service to /run/user/UID/systemd/generator/generated-service-name.service

Then home-manager creates a link to the linkFarm file (which is itself a link). The end result is a chain of links:

~/.config/systemd/user/servicename.service
-> /nix/store/sn55a0wl1mxy0fnq74mqrpv0jbc97309-home-manager-files/.config/systemd/user/servicename.service
-> /nix/store/mm66x07g2jld7497s7pf8cfl3l948p4c-user-quadlet-service-symlinks/servicename.service
-> /run/user/3008/systemd/generator/servicename.service

Happy to improve it, I'm just not sure how at this point.

Copy link
Author

Choose a reason for hiding this comment

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

On second thought perhaps using home-manager's systemd.user.tmpfiles.rules would work? We'd still have to reference the user's UID somehow.

Copy link
Owner

Choose a reason for hiding this comment

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

I gave systemd tmpfiles a try ("L+ ${config.xdg.configHome}/foo - - - - %t/foo"), it worked well creating the links but I can't make it delete those link when the config is removed.

I think linkFarm is fine here but getting uid from config isn't ideal as not everyone sets it. Since I don't have a good solution either (except for resorting to pkgs.runCommandLocal or activation script), I'm ok to merge the current version for now provided we make it a documented limitation (comment and README).

};
# Inject X-RestartIfChanged=${hash} for NixOS to detect changes.
"systemd/user/${p._unitName}.d/override.conf" = {
text = "[Unit]\nX-RestartIfChanged=${builtins.hashString "sha256" p._configText}";
};
}) allObjects
);
systemd.user.services.podman-auto-update = lib.mkIf cfg.autoUpdate.enable {
Unit = {
Description = "Podman auto-update service";
Documentation = "man:podman-auto-update(1)";
};
Service = {
Type = "oneshot";
# podman rootless requires "newuidmap" (the suid version, not the non-suid one from pkgs.shadow)
Environment = "PATH=/run/wrappers/bin";
ExecStart = "${pkgs.podman}/bin/podman auto-update";
ExecStartPost = "${pkgs.podman}/bin/podman image prune -f";
TimeoutStartSec = "900s";
TimeoutStopSec = "10s";
};
};
systemd.user.timers.podman-auto-update = lib.mkIf cfg.autoUpdate.enable {
Unit = {
Description = "Podman auto-update timer";
Documentation = "man:podman-auto-update(1)";
};
Timer = {
OnCalendar = cfg.autoUpdate.calendar;
Persistent = true;
};
Install.WantedBy = [ "timers.target" ];
};
};
}
10 changes: 9 additions & 1 deletion nixos-module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ in {
allObjects = (attrValues cfg.containers) ++ (attrValues cfg.networks);
in {
virtualisation.podman.enable = true;
environment.etc = mergeAttrsList (
environment.etc =
# Ensure podman-user-generator is available for systemd user services.
{
"systemd/user-generators/podman-user-generator" = {
source = "${pkgs.podman}/lib/systemd/user-generators/podman-user-generator";
target = "systemd/user-generators/podman-user-generator";
};
}
// mergeAttrsList (
map (p: {
"containers/systemd/${p._configName}" = {
text = p._configText;
Expand Down