Repository: SEIAROTg/quadlet-nix
Branch: main
Commit: 5e7016827231
Files: 33
Total size: 117.3 KB
Directory structure:
gitextract_vh7gx8rf/
├── .github/
│ └── workflows/
│ └── test.yml
├── LICENSE
├── README.md
├── build.nix
├── container.nix
├── docs/
│ ├── README.md
│ ├── flake.nix
│ └── src/
│ └── SUMMARY.md
├── flake.nix
├── home-manager-module.nix
├── image.nix
├── network.nix
├── nixos-module.nix
├── options.nix
├── pod.nix
├── tests/
│ ├── README.md
│ ├── aarch64-linux/
│ │ └── flake.nix
│ ├── basic.nix
│ ├── build.nix
│ ├── container.nix
│ ├── escaping.nix
│ ├── flake.nix
│ ├── health.nix
│ ├── image.nix
│ ├── network.nix
│ ├── overriding.nix
│ ├── pod.nix
│ ├── raw.nix
│ ├── switch.nix
│ ├── volume.nix
│ └── x86_64-linux/
│ └── flake.nix
├── utils.nix
└── volume.nix
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/test.yml
================================================
name: test
on:
push:
pull_request:
schedule:
- cron: '0 16 * * *' # UTC 16:00 daily
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
lazy-trees = true
eval-cores = 0
- run: nix run nixpkgs#nixfmt-tree -- --ci
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# TODO: re-enable aarch64-linux once github native runner supports nested virtualization.
system: [x86_64-linux]
version:
- nixpkgs: nixos-25.11
home-manager: release-25.11
- nixpkgs: nixos-unstable
home-manager: master
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: DeterminateSystems/determinate-nix-action@v3
with:
extra-conf: |
lazy-trees = true
eval-cores = 0
- uses: endersonmenezes/free-disk-space@v3
with:
remove_android: true
remove_dotnet: true
remove_haskell: true
rm_cmd: rmz
rmz_version: 3.1.1
- uses: cachix/cachix-action@v16
env:
CACHIX_AUTH_TOKEN_PRESENT: ${{ secrets.CACHIX_AUTH_TOKEN != '' }}
if: ${{ env.CACHIX_AUTH_TOKEN_PRESENT == 'true' }}
with:
name: quadlet-nix
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Run tests
run: >
nix flake check
--keep-going
--all-systems
--override-input nixpkgs 'github:NixOS/nixpkgs/${{ matrix.version.nixpkgs }}'
--override-input home-manager 'github:nix-community/home-manager/${{ matrix.version.home-manager }}'
--override-input test-config "path:$(pwd)/tests/${{ matrix.system }}"
./tests
- name: Build docs
run: |
nix build ./docs#book
- name: Upload docs
if: matrix.system == 'x86_64-linux' && matrix.version.nixpkgs == 'nixos-unstable'
uses: actions/upload-pages-artifact@v3
with:
path: ./result
pass:
needs: [format, test]
runs-on: ubuntu-slim
steps:
- run: true
publish-docs:
if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
needs: pass
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- uses: actions/deploy-pages@v4
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 SEIAROTg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# quadlet-nix
Manages Podman containers, networks, pods, etc. on NixOS via [Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html).
## Features
- Supports Podman containers, networks, pods, volumes, etc.
- Supports declarative update and deletion of networks.
- Supports rootful and rootless (via [Home Manager](https://github.com/nix-community/home-manager)) resources behind the same interface.
- Supports [Podman auto-update][podman-auto-update].
- Supports cross-referencing between resources in Nix language.
- Full quadlet options support, typed and properly escaped.
- Reliability through effective testing.
- Simplicity.
- Whatever offered by Nix or Quadlet.
[podman-auto-update]: https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html
## Motivation
This project was started in Aug 2023, as a result of [the author's frustration on some relatively simple container management needs](https://seiarotg.me/post/tidy-up-homelab-containers/), where then available technologies are either overly restrictive, or overly complex that requires non-trivial but pointless investment ad-hoc domain knowledge.
`quadlet-nix` is designed to be a simple tool that just works. Quadlet options are directly mapped into Nix, allowing users to effectively manage their Podman resources in the Nix language, without having to acquire domain knowledge in yet another tool. Prior knowledge and documentation of Podman continue to apply.
## Comparison
Below are comparisons with several alternatives for declaratively managing Podman containers on NixOS, effective as of May 2025.
<details>
<summary><a href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/oci-containers.nix" target="_blank">NixOS <code>virtualisation.oci-containers</code></a></summary>
- 👍 Part of NixOS, no additional dependencies.
- 👍 Rootless container support without additional dependencies.
- 👍 Supports Docker.
- 😐 Compatible with podman auto-update (requires external setup).
- 👎 Limited options.
- 👎 Lack of support for networks, pods, etc.
</details>
<details>
<summary><a href="https://github.com/hercules-ci/arion" target="_blank"><code>arion</code></a></summary>
- 👍 Supports Docker.
- 😐 More indirection and moving parts.
- 👎 Limited options.
- 👎 Incompatible with podman auto-update.
</details>
<details>
<summary><a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html" target="_blank">Vanilla Podman Quadlet</a></summary>
- 👍 Even less indirection.
- 😐 Compatible with podman auto-update (requires external setup).
- 😐 Requires more work to set up.
- 👎 Not integrated with rest of Nix configuration.
</details>
<details>
<summary><a href="https://nix-community.github.io/home-manager/options.xhtml#opt-services.podman.enable" target="_blank">Home Manager <code>services.podman</code></a></summary>
- 👍 Part of Home Manager, no additional dependencies if you are already using it.
- 👎 Lack of rootful container support.
</details>
<details>
<summary><a href="https://github.com/aksiksi/compose2nix" target="_blank"><code>compose2nix</code></a></summary>
- 👍 Supports Docker.
- 😐 Compatible with podman auto-update (requires external setup).
- 😐 More indirection and moving parts.
- 👎 Less maintainable Nix files due to generated boilerplate.
- 👎 Manual regeneration is required.
- 👎 Lack of rootless container support.
- 👎 Limited options.
- 👎 Fragmented configuration with source of truth being outside of Nix.
</details>
## How
See [seiarotg.github.io/quadlet-nix](https://seiarotg.github.io/quadlet-nix) for all options.
## Recipes
<details open>
<summary>Rootful containers</summary>
#### `flake.nix`
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
};
outputs = { nixpkgs, quadlet-nix, ... }@attrs: {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
quadlet-nix.nixosModules.quadlet
];
};
};
}
```
#### `configuration.nix`
```nix
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) networks pods;
in {
containers = {
nginx.containerConfig.image = "docker.io/library/nginx:latest";
nginx.containerConfig.networks = [ "podman" networks.internal.ref ];
nginx.containerConfig.pod = pods.foo.ref;
nginx.serviceConfig.TimeoutStartSec = "60";
};
networks = {
internal.networkConfig.subnets = [ "10.0.123.1/24" ];
};
pods = {
foo = { };
};
};
}
```
</details>
<details>
<summary>Rootless containers (via Home Manager)</summary>
#### `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";
};
outputs = { nixpkgs, quadlet-nix, home-manager, ... }@attrs: {
nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
# to enable podman & podman systemd generator
quadlet-nix.nixosModules.quadlet
];
};
};
}
```
#### `configuration.nix`
```nix
{
# ...
# to enable podman & podman systemd generator
virtualisation.quadlet.enable = true;
users.users.alice = {
# ...
# required for auto start before user login
linger = true;
# required for rootless container with multiple users
autoSubUidGidRange = true;
};
home-manager.users.alice = { pkgs, config, ... }: {
# ...
imports = [ inputs.quadlet-nix.homeManagerModules.quadlet ];
virtualisation.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";
};
};
};
};
}
```
</details>
<details>
<summary>Rootless containers (in system systemd)</summary>
⚠️ Not officially supported by Podman. Use at your own risk and expect breaking changes.
```nix
{ config, ... }: {
users.users.alice = {
uid = 1234;
# required for auto start before user login
linger = true;
# required for rootless container with multiple users
autoSubUidGidRange = true;
};
virtualisation.quadlet.containers.nginx = {
rootlessConfig.uid = config.users.users.alice.uid;
containerConfig.image = "docker.io/library/nginx:latest";
};
}
```
</details>
<details>
<summary>Volumes</summary>
```nix
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) volumes;
in {
containers.nginx.containerConfig.image = "docker.io/library/nginx:latest";
containers.nginx.containerConfig.volumes = [
"${volumes.nginx-config.ref}:/etc/nginx"
];
volumes.nginx-config.volumeConfig = {
type = "bind";
device = "/path/to/host/directory";
};
};
}
```
</details>
<details>
<summary>Build (inlined <code>Containerfile</code>)</summary>
```nix
{ pkgs, config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) builds;
containerfile = pkgs.writeText "Containerfile" ''
FROM docker.io/library/nginx:latest
# ...
'';
in {
containers.nginx.containerConfig.image = builds.nginx.ref;
builds.nginx.buildConfig.file = containerfile.outPath;
};
}
```
</details>
<details>
<summary>Build (git repository)</summary>
```nix
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) builds;
src = builtins.fetchGit {
url = "https://github.com/alpinelinux/docker-alpine.git";
rev = "4dc13cbc7caffe03c98aa99f28e27c2fb6f7e74d";
};
in {
containers.example.containerConfig = {
image = builds.alpine.ref;
entrypoint = "/bin/sh";
exec = "-c 'echo 123'";
};
containers.example.serviceConfig.RemainAfterExit = true;
builds.alpine.buildConfig = {
tag = "alpine:3.22";
workdir = "${src}/x86_64";
};
};
}
```
Alternatively, git integration of Podman can be used through `workdir = "https://github.com/nginx/docker-nginx.git"`. However, it will be users' responsibility to make binaries such as `git` available to the build service via `PATH`.
</details>
<details>
<summary>Image</summary>
```nix
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) images;
in {
containers.nginx.containerConfig.image = images.nginx.ref;
images.nginx.imageConfig.image = "docker-archive:/path/to/local/image";
images.nginx.imageConfig.tag = "docker.com/library/nginx:latest";
};
}
```
</details>
<details>
<summary>Install raw Quadlet files</summary>
If you wish to write raw Quadlet files instead of using the Nix options, you may do so with `rawConfig`. Using this will cause all other options (except `autoStart`) to be ignored though.
```nix
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) networks pods;
in {
containers = {
nginx.rawConfig = ''
[Container]
Image=docker.io/library/nginx:latest
Network=podman
Network=${networks.internal.ref}
Pod=${pods.foo.ref}
[Service]
TimeoutStartSec=60
'';
};
networks = {
internal.networkConfig.subnets = [ "10.0.123.1/24" ];
};
pods = {
foo = { };
};
};
}
```
</details>
<details>
<summary>Work with <code>pkgs.dockerTools</code></summary>
Podman natively supports multiple transport, including `docker-archive` that can be used with `pkgs.dockerTools`.
```nix
{ pkgs, ... }: let
image = pkgs.dockerTools.buildImage {
# ...
};
in {
virtualisation.quadlet.containers = {
foo.containerConfig.image = "docker-archive:${image}";
};
}
```
See: https://docs.podman.io/en/v5.5.0/markdown/podman-run.1.html#image
</details>
<details>
<summary>Podman DNS not working?</summary>
To use Podman DNS, it needs to be enabled and allowed by your firewall.
For the default network, below sets up both for you:
```nix
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
```
Or if you manage firewall separately, allow UDP port 53 on the input chain on host interface "podman0" and set:
```nix
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
virtualisation.podman.defaultNetwork.settings.network_interface = "podman0";
```
For custom networks managed by Quadlet, Podman DNS is enabled by default, unless `disableDns` is set. To set up the firewall rules:
```nix
virtualisation.quadlet.networks.foo.networkConfig.interfaceName = "br-foo";
networking.firewall.interfaces.br-foo.allowedUDPPorts = [ 53 ];
```
To apply this on all custom networks:
#### `enable-dns.nix`
```nix
{ config, lib, ... }: {
options.virtualisation.quadlet.networks = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ( { name, ... }: {
networkConfig.driver = "bridge";
networkConfig.interfaceName = "br-${name}";
}));
};
config.networking.firewall.interfaces = lib.mapAttrs' (name: _: {
name = "br-${name}";
value.allowedUDPPorts = [ 53 ];
}) config.virtualisation.quadlet.networks;
}
```
#### `configuration.nix`
```nix
{
imports = [
./enable-dns.nix
];
# ...
}
```
</details>
<details>
<summary>Dependencies</summary>
Obvious dependencies such as those between containers and their networks are automatically set up by Quadlet, and thus no additional configuration is needed.
Extra dependencies can be set up in systemd unit config. Note that `.ref` syntax is only valid in quadlet and does not work from regular systemd units.
```nix
{ config, ... }: {
# ...
virtualisation.quadlet = let
inherit (config.virtualisation.quadlet) containers;
in {
containers = {
database = {
# ...
};
server = {
# ...
unitConfig.Requires = [ containers.database.ref "network-online.target" ];
unitConfig.After = [ containers.database.ref "network-online.target" ];
};
};
};
}
```
</details>
<details>
<summary>Debug & log access</summary>
`quadlet-nix` tries to put containers into full management under systemd. This means once a container crashes, it will be fully deleted and debugging mechanisms like `podman ps -a` or `podman logs` will not work.
However, status and logs are still accessible through systemd, namely, `systemctl status <service name>` and `journalctl -u <service name>`, where `<service name>` is container name, `<network name>-network`, `<pod name>-pod`, or similar. These names are the names as appeared in `virtualisation.quadlet.containers.<container name>`, rather than podman container name, in case it's different.
</details>
<details>
<summary>The option I need is not available</summary>
Check if that option is supported by Podman Quadlet here: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html.
If it exists, please create an issue or send a PR to add.
Otherwise, please use `PodmanArgs` and `GlobalArgs` to insert additional command line arguments as `quadlet-nix` does not intend to support options beyond what Quadlet offers.
</details>
================================================
FILE: build.nix
================================================
{ quadletUtils, quadletOptions }:
{
config,
name,
lib,
...
}:
let
inherit (lib) types;
inherit (quadletUtils) encoders;
buildOpts = {
annotations = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
annotation = "value";
};
cli = "--annotation";
property = "Annotation";
encoders.scalar = encoders.scalar.quotedEscaped;
};
arch = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "aarch64";
cli = "--arch";
property = "Arch";
};
authFile = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/etc/registry/auth.json";
cli = "--authfile";
property = "AuthFile";
};
buildArgs = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
foo = "bar";
};
cli = "--build-arg";
property = "BuildArg";
encoders.scalar = encoders.scalar.quotedEscaped;
};
modules = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/etc/nvd.conf" ];
cli = "--module";
property = "ContainersConfModule";
};
dns = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.55.1" ];
cli = "--dns";
property = "DNS";
};
dnsSearch = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "foo.com" ];
cli = "--dns-search";
property = "DNSSearch";
};
dnsOption = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "ndots:1" ];
cli = "--dns-option";
property = "DNSOption";
};
environments = quadletOptions.mkOption {
type = types.attrsOf types.str;
default = { };
example = {
foo = "bar";
};
cli = "--env";
property = "Environment";
encoders.scalar = encoders.scalar.quotedEscaped;
};
file = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/path/to/Containerfile";
cli = "--file";
property = "File";
};
forceRm = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--force-rm";
property = "ForceRM";
};
globalArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--log-level=debug" ];
description = "Additional command line arguments to insert between `podman` and `build`";
property = "GlobalArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
addGroups = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "keep-groups" ];
cli = "--group-add";
property = "GroupAdd";
};
ignoreFile = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/path/to/.customignore";
cli = "--ignorefile";
property = "IgnoreFile";
};
tag = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "localhost/imagename";
cli = "--tag";
property = "ImageTag";
};
labels = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
foo = "bar";
};
cli = "--label";
property = "Label";
encoders.scalar = encoders.scalar.quotedEscaped;
};
networks = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "host" ];
cli = "--net";
property = "Network";
};
podmanArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--add-host foobar" ];
description = "Additional command line arguments to insert after `podman build`";
property = "PodmanArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
pull = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "never";
cli = "--pull";
property = "Pull";
};
retry = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 5;
cli = "--retry";
property = "Retry";
};
retryDelay = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "5s";
cli = "--retry-delay";
property = "RetryDelay";
};
secrets = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "secret[,opt=opt …]" ];
cli = "--secret";
property = "Secret";
encoders.scalar = encoders.scalar.quotedEscaped;
};
workdir = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "file";
description = "Sets WorkingDirectory of systemd unit file";
property = "SetWorkingDirectory";
};
target = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "my-app";
cli = "--target";
property = "Target";
};
tlsVerify = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--tls-verify";
property = "TLSVerify";
};
variant = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "arm/v7";
cli = "--variant";
property = "Variant";
};
volumes = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/source:/dest" ];
cli = "--volume";
property = "Volume";
};
};
serviceConfigDefault = {
TimeoutStartSec = 900;
};
in
{
options = quadletOptions.mkObjectOptions "build" {
buildConfig = buildOpts;
};
config =
let
buildTag = if config.buildConfig.tag != null then config.buildConfig.tag else "localhost/${name}";
buildConfig = config.buildConfig // {
tag = buildTag;
};
quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
unitConfig = {
Unit = {
Description = "Podman build ${name}";
}
// config.unitConfig;
Build = quadletUtils.configToProperties buildConfig buildOpts;
Service = serviceConfigDefault // config.serviceConfig;
}
// (if quadlet == { } then { } else { Quadlet = quadlet; });
in
lib.pipe
{
_serviceName = "${name}-build";
_configText =
if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
_autoStart = config.autoStart;
_autoEscapeRequired = quadletUtils.autoEscapeRequired buildConfig buildOpts;
ref = "${name}.build";
}
[
(quadletOptions.applyRootlessConfig config)
];
}
================================================
FILE: container.nix
================================================
{ quadletUtils, quadletOptions }:
{
config,
name,
lib,
...
}:
let
inherit (lib) types;
inherit (quadletUtils) encoders;
containerOpts = {
addCapabilities = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "NET_ADMIN" ];
cli = "--cap-add";
property = "AddCapability";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
addHosts = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "hostname:192.168.10.11" ];
cli = "--add-host";
property = "AddHost";
};
devices = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/dev/foo" ];
cli = "--device";
property = "AddDevice";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
annotations = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
annotation = "value";
};
cli = "--annotation";
property = "Annotation";
encoders.scalar = encoders.scalar.quotedEscaped;
};
autoUpdate = quadletOptions.mkOption {
type = types.nullOr (
types.enum [
"registry"
"local"
]
);
default = null;
example = "registry";
cli = "--label \"io.containers.autoupdate=...\"";
property = "AutoUpdate";
};
appArmor = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "alternate-profile";
cli = "--security-opt apparmor=...";
property = "AppArmor";
};
cgroupsMode = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "no-conmon";
cli = "--cgroups";
property = "CgroupsMode";
};
name = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "name";
cli = "--name";
property = "ContainerName";
};
modules = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/etc/nvd.conf" ];
cli = "--module";
property = "ContainersConfModule";
};
dns = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.55.1" ];
cli = "--dns";
property = "DNS";
};
dnsSearch = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "foo.com" ];
cli = "--dns-search";
property = "DNSSearch";
};
dnsOption = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "ndots:1" ];
cli = "--dns-option";
property = "DNSOption";
};
dropCapabilities = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "NET_ADMIN" ];
cli = "--cap-drop";
property = "DropCapability";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
entrypoint = quadletOptions.mkOption {
type = types.nullOr (
types.oneOf [
types.str
(types.listOf types.str)
]
);
default = null;
example = "/foo.sh";
cli = "--entrypoint";
property = "Entrypoint";
encoders.raw = encoders.scalar.raw;
encoders.list = encoders.list.json;
};
environments = quadletOptions.mkOption {
type = types.attrsOf types.str;
default = { };
example = {
foo = "bar";
};
cli = "--env";
property = "Environment";
encoders.scalar = encoders.scalar.quotedEscaped;
};
environmentFiles = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/tmp/env" ];
cli = "--env-file";
property = "EnvironmentFile";
encoders.scalar = encoders.scalar.quotedEscaped;
};
environmentHost = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--env-host";
property = "EnvironmentHost";
};
exec = quadletOptions.mkOption {
type = types.nullOr (
types.oneOf [
types.str
(types.listOf types.str)
]
);
default = null;
example = "/usr/bin/command";
description = "Command after image specification";
property = "Exec";
# CAVEAT: doesn't prevent systemd environment variable substitution, but probably a quadlet problem?
encoders.scalar = encoders.scalar.raw;
encoders.list = encoders.list.oneLine encoders.scalar.quotedEscaped;
};
exposePorts = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "50-59" ];
cli = "--expose";
property = "ExposeHostPort";
};
gidMaps = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "0:10000:10" ];
cli = "--gidmap";
property = "GIDMap";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
globalArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--log-level=debug" ];
description = "Additional command line arguments to insert between `podman` and `run`";
property = "GlobalArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
group = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "1234";
cli = "--user UID:...";
property = "Group";
};
addGroups = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "keep-groups" ];
cli = "--group-add";
property = "GroupAdd";
};
healthCmd = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/usr/bin/command";
cli = "--health-cmd";
property = "HealthCmd";
};
healthInterval = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "2m";
cli = "--health-interval";
property = "HealthInterval";
};
healthLogDestination = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/foo/log";
cli = "--health-log-destination";
property = "HealthLogDestination";
};
healthMaxLogCount = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 5;
cli = "--health-max-log-count";
property = "HealthMaxLogCount";
};
healthMaxLogSize = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 500;
cli = "--health-max-log-size";
property = "HealthMaxLogSize";
};
healthOnFailure = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "kill";
cli = "--health-on-failure";
property = "HealthOnFailure";
};
healthRetries = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 5;
cli = "--health-retries";
property = "HealthRetries";
};
healthStartPeriod = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "1m";
cli = "--health-start-period";
property = "HealthStartPeriod";
};
healthStartupCmd = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/usr/bin/command";
cli = "--health-startup-cmd";
property = "HealthStartupCmd";
};
healthStartupInterval = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "1m";
cli = "--health-startup-interval";
property = "HealthStartupInterval";
};
healthStartupRetries = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 8;
cli = "--health-startup-retries";
property = "HealthStartupRetries";
};
healthStartupSuccess = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 2;
cli = "--health-startup-success";
property = "HealthStartupSuccess";
};
healthStartupTimeout = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "1m33s";
cli = "--health-startup-timeout";
property = "HealthStartupTimeout";
};
healthTimeout = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "20s";
cli = "--health-timeout";
property = "HealthTimeout";
};
hostname = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "new-host-name";
cli = "--hostname";
property = "HostName";
};
httpProxy = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--http-proxy";
property = "HttpProxy";
};
image = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "docker.io/library/nginx:latest";
description = "Image specification";
property = "Image";
};
ip = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "192.5.0.1";
cli = "--ip";
property = "IP";
};
ip6 = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "fd46:db93:aa76:ac37::10";
cli = "--ip6";
property = "IP6";
};
labels = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
foo = "bar";
};
cli = "--label";
property = "Label";
encoders.scalar = encoders.scalar.quotedEscaped;
};
logDriver = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "journald";
cli = "--log-driver";
property = "LogDriver";
};
logOptions = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "path=/var/log/mykube.json" ];
cli = "--log-opt";
property = "LogOpt";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
mask = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/proc/sys/foo:/proc/sys/bar";
cli = "--security-opt mask=...";
property = "Mask";
encoders.scalar = encoders.scalar.quotedEscaped;
};
memory = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "20g";
cli = "--memory";
property = "Memory";
};
mounts = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "type=..." ];
cli = "--mount";
property = "Mount";
encoders.scalar = encoders.scalar.quotedEscaped;
};
networks = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "host" ];
cli = "--net";
property = "Network";
};
networkAliases = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "name" ];
cli = "--network-alias";
property = "NetworkAlias";
};
noNewPrivileges = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--security-opt no-new-privileges";
property = "NoNewPrivileges";
};
notify = quadletOptions.mkOption {
type = types.enum [
null
true
false
"healthy"
];
default = null;
cli = "--sdnotify container";
property = "Notify";
};
pidsLimit = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 10000;
cli = "--pids-limit";
property = "PidsLimit";
};
pod = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
cli = "--pod";
property = "Pod";
};
podmanArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--add-host foobar" ];
description = "Additional command line arguments to insert after `podman run`";
property = "PodmanArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
publishPorts = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "50-59" ];
cli = "--publish";
property = "PublishPort";
};
pull = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "never";
cli = "--pull";
property = "Pull";
};
readOnly = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--read-only";
property = "ReadOnly";
};
readOnlyTmpfs = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--read-only-tmpfs";
property = "ReadOnlyTmpfs";
};
reloadCmd = quadletOptions.mkOption {
type = types.nullOr (
types.oneOf [
types.str
(types.listOf types.str)
]
);
default = null;
description = "Adds ExecReload and run exec with the value";
example = "/usr/bin/command";
property = "ReloadCmd";
encoders.scalar = encoders.scalar.raw;
encoders.list = encoders.list.oneLine encoders.scalar.quotedEscaped;
};
reloadSignal = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
description = "Add ExecReload and run kill with the signal";
example = "SIGHUP";
property = "ReloadSignal";
};
retry = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 5;
cli = "--retry";
property = "Retry";
};
retryDelay = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "5s";
cli = "--retry-delay";
property = "RetryDelay";
};
rootfs = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/var/lib/rootfs";
cli = "--rootfs";
property = "Rootfs";
};
runInit = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--init";
property = "RunInit";
};
seccompProfile = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/tmp/s.json";
cli = "--security-opt seccomp=...";
property = "SeccompProfile";
};
secrets = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "secret[,opt=opt …]" ];
cli = "--secret";
property = "Secret";
encoders.scalar = encoders.scalar.quotedEscaped;
};
securityLabelDisable = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--security-opt label=disable";
property = "SecurityLabelDisable";
};
securityLabelFileType = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "usr_t";
cli = "--security-opt label=filetype:...";
property = "SecurityLabelFileType";
};
securityLabelLevel = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "s0:c1,c2";
cli = "--security-opt label=level:s0:c1,c2";
property = "SecurityLabelLevel";
};
securityLabelNested = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--security-opt label=nested";
property = "SecurityLabelNested";
};
securityLabelType = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "spc_t";
cli = "--security-opt label=type:...";
property = "SecurityLabelType";
};
shmSize = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "100m";
cli = "--shm-size";
property = "ShmSize";
};
startWithPod = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
description = "If pod is defined, container is started by pod";
property = "StartWithPod";
};
stopSignal = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "SIGINT";
cli = "--stop-signal";
property = "StopSignal";
};
stopTimeout = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 20;
cli = "--stop-timeout";
property = "StopTimeout";
};
subGIDMap = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "gtest";
cli = "--subgidname";
property = "SubGIDMap";
};
subUIDMap = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "utest";
cli = "--subuidname";
property = "SubUIDMap";
};
sysctl = quadletOptions.mkOption {
type = types.attrsOf types.str;
default = { };
example = {
name = "value";
};
cli = "--sysctl";
property = "Sysctl";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
timezone = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "local";
cli = "--tz";
property = "Timezone";
};
tmpfses = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/work" ];
cli = "--tmpfs";
property = "Tmpfs";
};
uidMaps = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "0:10000:10" ];
cli = "--uidmap";
property = "UIDMap";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
ulimits = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "nofile=1000:10000" ];
cli = "--ulimit";
property = "Ulimit";
};
unmask = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "ALL";
cli = "--security-opt unmask=...";
property = "Unmask";
encoders.scalar = encoders.scalar.quotedEscaped;
};
user = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "bin";
cli = "--user";
property = "User";
};
userns = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "keep-id:uid=200,gid=210";
cli = "--userns";
property = "UserNS";
};
volumes = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/source:/dest" ];
cli = "--volume";
property = "Volume";
};
workdir = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "$HOME";
cli = "--workdir";
property = "WorkingDir";
};
};
serviceConfigDefault = {
Restart = "always";
TimeoutStartSec = 900;
};
in
{
options = quadletOptions.mkObjectOptions "container" {
containerConfig = containerOpts;
};
config =
let
containerName = if config.containerConfig.name != null then config.containerConfig.name else name;
containerConfig = config.containerConfig // {
name = containerName;
};
quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
unitConfig = {
Unit = {
Description = "Podman container ${name}";
}
// config.unitConfig;
Container = quadletUtils.configToProperties containerConfig containerOpts;
Service = serviceConfigDefault // config.serviceConfig;
}
// (if quadlet == { } then { } else { Quadlet = quadlet; });
in
lib.pipe
{
_serviceName = name;
_configText =
if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
_autoStart = config.autoStart;
_autoEscapeRequired = quadletUtils.autoEscapeRequired containerConfig containerOpts;
ref = "${name}.container";
# quadlet default is "split" which does not work rootless under system systemd.
containerConfig.cgroupsMode = lib.mkIf config._rootless (lib.mkDefault "enabled");
}
[
(quadletOptions.applyRootlessConfig config)
];
}
================================================
FILE: docs/README.md
================================================
# Docs
To generate the documentation, run:
```sh
nix build './docs#book'
```
================================================
FILE: docs/flake.nix
================================================
{
description = "quadlet-nix docs";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
quadlet-nix.url = "path:..";
};
outputs =
{
nixpkgs,
quadlet-nix,
self,
...
}:
let
allSystems = [
"x86_64-linux"
"aarch64-linux"
];
perSystem = f: nixpkgs.lib.genAttrs allSystems f;
in
{
packages = perSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
buildDocs =
module:
let
moduleFn = import module;
# filters out assertions, config, etc. that cause problems.
filteredModuleFn = args: { inherit (moduleFn args) options; };
eval = lib.evalModules {
modules = [
{ _module.args.pkgs = pkgs; }
(lib.mirrorFunctionArgs moduleFn filteredModuleFn)
];
};
options = lib.filterAttrs (name: _: name != "_module") eval.options;
in
pkgs.nixosOptionsDoc { inherit options; };
pages = {
nixosModules.quadlet = buildDocs quadlet-nix.nixosModules.quadlet;
homeManagerModules.quadlet = buildDocs quadlet-nix.homeManagerModules.quadlet;
};
in
{
inherit pages;
book = pkgs.stdenv.mkDerivation {
pname = "quadlet-nix-docs-book";
version = "0.1";
src = self;
nativeBuildInputs = [
pkgs.mdbook
];
dontConfigure = true;
dontFixup = true;
buildPhase = ''
runHook preBuild
cp ${quadlet-nix}/README.md src/introduction.md
cp ${pages.nixosModules.quadlet.optionsCommonMark} src/nixos-options.md
cp ${pages.homeManagerModules.quadlet.optionsCommonMark} src/home-manager-options.md
mdbook build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mv book $out
runHook postInstall
'';
};
}
);
};
}
================================================
FILE: docs/src/SUMMARY.md
================================================
# Contents
- [Introduction](./introduction.md)
- [NixOS Options](./nixos-options.md)
- [Home Manager Options](./home-manager-options.md)
================================================
FILE: flake.nix
================================================
{
description = "NixOS and home-manager module for Podman Quadlets";
outputs =
{ self }:
{
nixosModules.quadlet = ./nixos-module.nix;
homeManagerModules.quadlet = ./home-manager-module.nix;
};
}
================================================
FILE: home-manager-module.nix
================================================
{
config,
osConfig ? { },
lib,
pkgs,
...
}:
let
inherit (lib) mergeAttrsList mkIf getExe;
cfg = config.virtualisation.quadlet;
quadletUtils = import ./utils.nix {
inherit pkgs lib;
inherit (import (pkgs.path + "/nixos/lib/utils.nix") { inherit lib config pkgs; }) systemdUtils;
podmanPackage = osConfig.virtualisation.podman.package or pkgs.podman;
autoEscape = config.virtualisation.quadlet.autoEscape;
};
quadletOptions = import ./options.nix {
supportRootless = false;
inherit lib quadletUtils;
};
activationScript = lib.hm.dag.entryBefore [ "reloadSystemd" ] ''
mkdir -p '${config.xdg.configHome}/quadlet-nix/'
ln -sf "''${XDG_RUNTIME_DIR:-/run/user/$UID}/systemd/generator/" '${config.xdg.configHome}/quadlet-nix/out'
'';
in
{
options.virtualisation.quadlet = quadletOptions.mkTopLevelOptions { };
config =
let
allObjects = quadletOptions.getAllObjects cfg;
enable = cfg.enable == true || (cfg.enable == null && allObjects != [ ]);
in
mkIf enable {
assertions = quadletOptions.mkAssertions [ ] cfg;
warnings =
(quadletUtils.assertionsToWarnings [
{
assertion = enable -> (osConfig.virtualisation.quadlet.enable or true == true);
message = ''
The `virtualisation.quadlet.enable` in **NixOS config** is not set to true.
The NixOS module is required to set up Podman and explicit enablement will be required in the future.
'';
}
])
++ (quadletOptions.mkWarnings [ ] cfg);
home.activation.quadletNix = mkIf (lib.length allObjects > 0) activationScript;
xdg.configFile =
let
configPathLink =
(pkgs.linkFarm "quadlet-out-path" [
{
name = "quadlet-nix";
path = "${config.xdg.configHome}/quadlet-nix";
}
])
+ "/quadlet-nix";
in
mergeAttrsList (
map (p: {
# Install the .container, .network, etc files
"containers/systemd/${p.ref}" = {
text = p._configText;
};
# Import quadlet-generated unit as a dropin override.
"systemd/user/${p._serviceName}.service.d/override.conf" = {
source = "${configPathLink}/out/${p._serviceName}.service";
};
}) allObjects
)
// {
# `systemctl`, `sleep`, etc. not found
"systemd/user/podman-user-wait-network-online.service.d/override.conf" = {
text = quadletUtils.unitConfigToText {
Service.ExecSearchPath = [ "/run/current-system/sw/bin/" ];
};
};
};
systemd.user.services =
mergeAttrsList (
map (p: {
# Inject hash for the activation process to detect changes.
# Must be in the main file as it's the only thing home-manager switch process looks at.
# WantedBy must be set through `systemd.user.services` which generates .targets.wants symlinks.
# sd-switch only starts new services with those symlinks.
${p._serviceName} = {
Unit.X-QuadletNixConfigHash = builtins.hashString "sha256" p._configText;
Install.WantedBy = if p._autoStart then [ "default.target" ] else [ ];
};
}) allObjects
)
// {
# TODO: link from ${pkgs.podman}/share/systemd/user/podman-auto-update.service
# when https://github.com/containers/podman/issues/24637 is fixed.
podman-auto-update = mkIf cfg.autoUpdate.enable {
Unit = {
Description = "Podman auto-update service";
Documentation = "man:podman-auto-update(1)";
};
Service = {
Type = "oneshot";
ExecStart = "${getExe quadletUtils.podmanPackage} auto-update";
ExecStartPost = "${getExe quadletUtils.podmanPackage} image prune -f";
TimeoutStartSec = "900s";
TimeoutStopSec = "10s";
};
};
};
systemd.user.timers.podman-auto-update = 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" ];
};
};
}
================================================
FILE: image.nix
================================================
{ quadletUtils, quadletOptions }:
{
config,
name,
lib,
...
}:
let
inherit (lib) types;
inherit (quadletUtils) encoders;
imageOpts = {
allTags = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--all-tags";
property = "AllTags";
};
arch = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "aarch64";
cli = "--arch";
property = "Arch";
};
authFile = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/etc/registry/auth.json";
cli = "--authfile";
property = "AuthFile";
};
certDir = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/etc/registry/certs";
cli = "--cert-dir";
property = "CertDir";
};
modules = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/etc/nvd.conf" ];
cli = "--module";
property = "ContainersConfModule";
};
creds = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "myname:mypassword";
cli = "--creds";
property = "Creds";
};
decryptionKey = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "/etc/registry.key";
cli = "--decryption-key";
property = "DecryptionKey";
};
globalArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--log-level=debug" ];
description = "Additional command line arguments to insert between `podman` and `pull`";
property = "GlobalArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
image = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "docker.io/library/nginx:latest";
description = "Image specification";
property = "Image";
};
tag = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "localhost/imagename";
description = "FQIN of the referenced Image. Only meaningful when source is a file or directory archive. Used when resolving .image references.";
property = "ImageTag";
};
os = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "windows";
cli = "--os";
property = "OS";
};
podmanArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--add-host foobar" ];
description = "Additional command line arguments to insert after `podman pull`";
property = "PodmanArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
policy = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "always";
cli = "--policy";
property = "Policy";
};
retry = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 5;
cli = "--retry";
property = "Retry";
};
retryDelay = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "5s";
cli = "--retry-delay";
property = "RetryDelay";
};
tlsVerify = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--tls-verify";
property = "TLSVerify";
};
variant = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "arm/v7";
cli = "--variant";
property = "Variant";
};
};
serviceConfigDefault = {
TimeoutStartSec = 900;
};
in
{
options = quadletOptions.mkObjectOptions "image" {
imageConfig = imageOpts;
};
config =
let
imageConfig = config.imageConfig;
quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
unitConfig = {
Unit = {
Description = "Podman image ${name}";
}
// config.unitConfig;
Image = quadletUtils.configToProperties imageConfig imageOpts;
Service = serviceConfigDefault // config.serviceConfig;
}
// (if quadlet == { } then { } else { Quadlet = quadlet; });
in
lib.pipe
{
_serviceName = "${name}-image";
_configText =
if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
_autoStart = config.autoStart;
_autoEscapeRequired = quadletUtils.autoEscapeRequired imageConfig imageOpts;
ref = "${name}.image";
}
[
(quadletOptions.applyRootlessConfig config)
];
}
================================================
FILE: network.nix
================================================
{ quadletUtils, quadletOptions }:
{
config,
name,
lib,
...
}:
let
inherit (lib) types getExe;
inherit (quadletUtils) encoders;
networkOpts = {
modules = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/etc/nvd.conf" ];
cli = "--module";
property = "ContainersConfModule";
};
disableDns = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--disable-dns";
property = "DisableDNS";
};
dns = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.55.1" ];
cli = "--dns";
property = "DNS";
};
driver = quadletOptions.mkOption {
type = types.nullOr (
types.enum [
"bridge"
"macvlan"
"ipvlan"
]
);
default = null;
example = "bridge";
cli = "--driver";
property = "Driver";
};
gateways = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.55.3" ];
cli = "--gateway";
property = "Gateway";
};
globalArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--log-level=debug" ];
description = "Additional command line arguments to insert between `podman` and `network create`";
property = "GlobalArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
interfaceName = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "enp1";
cli = "--interface-name";
property = "InterfaceName";
};
internal = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--internal";
property = "Internal";
};
ipamDriver = quadletOptions.mkOption {
type = types.nullOr (
types.enum [
"host-local"
"dhcp"
"none"
]
);
default = null;
example = "dhcp";
cli = "--ipam-driver";
property = "IPAMDriver";
};
ipRanges = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.55.128/25" ];
cli = "--ip-range";
property = "IPRange";
};
ipv6 = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--ipv6";
property = "IPv6";
};
labels = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
foo = "bar";
};
cli = "--label";
property = "Label";
encoders.scalar = encoders.scalar.quotedEscaped;
};
name = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "foo";
description = "Network name as in `podman network create foo`";
property = "NetworkName";
};
networkDeleteOnStop = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
description = "When set to true the network is deleted when the service is stopped";
property = "NetworkDeleteOnStop";
};
options = quadletOptions.mkOption {
# TODO: drop string support and remove warning.
type = types.oneOf [
types.str
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
isolate = "true";
};
cli = "--opt";
property = "Options";
encoders.scalar = encoders.scalar.quotedEscaped;
};
podmanArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--dns=192.168.55.1" ];
description = "Additional command line arguments to insert after `podman network create`";
property = "PodmanArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
subnets = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.5.0.0/16" ];
cli = "--subnet";
property = "Subnet";
};
};
in
{
options = quadletOptions.mkObjectOptions "network" {
networkConfig = networkOpts;
};
config =
let
networkName = if config.networkConfig.name != null then config.networkConfig.name else name;
networkConfig = config.networkConfig // {
name = networkName;
};
quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
unitConfig = {
Unit = {
Description = "Podman network ${name}";
}
// config.unitConfig;
Network = quadletUtils.configToProperties networkConfig networkOpts;
Service = {
# TODO: switches to NetworkDeleteOnStop once podman in stable nixpkgs supports it
ExecStop = "${getExe quadletUtils.podmanPackage} network rm ${networkName}";
}
// config.serviceConfig;
}
// (if quadlet == { } then { } else { Quadlet = quadlet; });
in
lib.pipe
{
_serviceName = "${name}-network";
_configText =
if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
_autoStart = config.autoStart;
_autoEscapeRequired = quadletUtils.autoEscapeRequired networkConfig networkOpts;
ref = "${name}.network";
}
[
(quadletOptions.applyRootlessConfig config)
];
}
================================================
FILE: nixos-module.nix
================================================
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mergeAttrsList mkIf;
cfg = config.virtualisation.quadlet;
quadletUtils = import ./utils.nix {
inherit pkgs lib;
inherit (import (pkgs.path + "/nixos/lib/utils.nix") { inherit lib config pkgs; }) systemdUtils;
podmanPackage = config.virtualisation.podman.package;
autoEscape = config.virtualisation.quadlet.autoEscape;
};
quadletOptions = import ./options.nix {
supportRootless = true;
inherit lib quadletUtils;
};
in
{
options.virtualisation.quadlet = quadletOptions.mkTopLevelOptions { };
config =
let
allObjects = quadletOptions.getAllObjects cfg;
# TODO: switch to `cfg.enable == true || (cfg.enable == null && allObjects != [])`
# when home-manager users set `enable` explicitly.
enable = cfg.enable == true || cfg.enable == null;
in
mkIf enable {
assertions = quadletOptions.mkAssertions [ ] cfg;
warnings = quadletOptions.mkWarnings [ ] cfg;
virtualisation.podman.enable = true;
environment.etc = mergeAttrsList (
map (p: {
"containers/systemd/${p.ref}" = {
text = p._configText;
mode = "0600";
};
}) allObjects
);
# The symlinks are not necessary for the services to be honored by systemd,
# but necessary for NixOS activation process to pick them up for updates.
systemd.packages = [
(pkgs.linkFarm "quadlet-service-symlinks" (
map (p: {
name = "etc/systemd/system/${p._serviceName}.service";
path = "/run/systemd/generator/${p._serviceName}.service";
}) allObjects
))
];
# Inject X-RestartIfChanged=${hash} for NixOS to detect changes.
systemd.services = mergeAttrsList (
map (p: {
${p._serviceName} = {
overrideStrategy = "asDropin";
unitConfig.X-QuadletNixConfigHash = builtins.hashString "sha256" p._configText;
# systemd recommends multi-user.target over default.target.
# https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html#default.target
wantedBy = if p._autoStart then [ "multi-user.target" ] else [ ];
}
// p._overrides;
}) allObjects
);
systemd.timers.podman-auto-update = mkIf cfg.autoUpdate.enable {
timerConfig.OnCalendar = [
""
cfg.autoUpdate.calendar
];
wantedBy = [ "timers.target" ];
overrideStrategy = "asDropin";
};
};
}
================================================
FILE: options.nix
================================================
{
lib,
quadletUtils,
supportRootless,
}:
let
mkOption =
{
property,
cli ? null,
description ? null,
encoders ? null,
...
}@attrs:
let
descForDesc = if description == null then "" else description + "\n\n";
descForCli = if cli == null then "" else "and command line argument `${cli}`";
in
(lib.mkOption (
lib.filterAttrs (
name: _:
!(builtins.elem name [
"property"
"cli"
"encoders"
])
) attrs
))
// {
inherit property;
inherit encoders;
description = "${descForDesc}Maps to quadlet option `${property}`${descForCli}.";
};
quadletOpts = {
defaultDependencies = mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = "Add Quadlet’s default network dependencies to the unit";
property = "DefaultDependencies";
};
};
mkCommonObjectOptions =
objectType:
{
quadletConfig = quadletOpts;
autoStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "When enabled, this ${objectType} is automatically started on boot.";
};
unitConfig = lib.mkOption {
type = lib.types.attrsOf quadletUtils.unitOption;
default = { };
description = "systemd unit config passed through to [Unit] section.";
};
serviceConfig = lib.mkOption {
type = lib.types.attrsOf quadletUtils.unitOption;
default = { };
description = "systemd service config passed through to [Service] section.";
};
rawConfig = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Raw quadlet config text. Using this will cause all other options
contributing to quadlet files to be ignored. autoStart is not affected.
'';
};
_serviceName = lib.mkOption {
internal = true;
description = "Name of the systemd service unit, without the .service suffix.";
};
_configText = lib.mkOption {
internal = true;
description = "Generated quadlet config text";
};
_autoStart = lib.mkOption {
internal = true;
description = "Whether the service is automatically started on boot.";
};
_autoEscapeRequired = lib.mkOption {
internal = true;
description = ''
Whether `autoEscape` needs to be switched on for correct encoding.
This is false if already on.
'';
};
_rootless = lib.mkOption {
internal = true;
default = false;
description = ''
Whether to run rootless under system systemd.
'';
};
_overrides = lib.mkOption {
internal = true;
default = { };
description = ''
Overrides to apply on systemd.services.<name>. Not applicable to user systemd.
'';
};
ref = lib.mkOption {
readOnly = true;
description = ''
Reference to this ${objectType} from other quadlets.
Quadlet resolves this to object (e.g. container) names and sets up appropriate systemd dependencies.
This is recognized for most quadlet native options, but not by Podman command line.
Using this inside `podmanArgs` will therefore unlikely to work.
'';
};
}
// (
if !supportRootless then
{ }
else
{
rootlessConfig = {
uid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = "User ID to run rootless podman as";
};
};
}
);
commonTopLevelOptions =
let
submoduleArgs = {
inherit quadletUtils;
quadletOptions = self;
};
in
{
builds = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule (import ./build.nix submoduleArgs));
default = { };
description = "Image builds";
};
containers = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule (import ./container.nix submoduleArgs));
default = { };
description = "Containers";
};
images = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule (import ./image.nix submoduleArgs));
default = { };
description = "Image pulls";
};
networks = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule (import ./network.nix submoduleArgs));
default = { };
description = "Networks";
};
pods = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule (import ./pod.nix submoduleArgs));
default = { };
description = "Pods";
};
volumes = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule (import ./volume.nix submoduleArgs));
default = { };
description = "Volumes";
};
enable = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = "Enables quadlet-nix";
};
autoEscape = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enables appropriate quoting / escaping.
'';
};
autoUpdate = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enables podman auto update.";
};
calendar = lib.mkOption {
type = lib.types.str;
default = "*-*-* 00:00:00";
description = "Schedule for podman auto update. See `systemd.time(7)` for details.";
};
};
};
getAllObjects =
config:
builtins.concatLists (
map lib.attrValues [
config.builds
config.containers
config.images
config.networks
config.pods
config.volumes
]
);
self = {
inherit mkOption quadletOpts;
mkObjectOptions =
objectType: extraOptions:
lib.attrsets.unionOfDisjoint (mkCommonObjectOptions objectType) extraOptions;
mkTopLevelOptions = extraOptions: lib.attrsets.unionOfDisjoint commonTopLevelOptions extraOptions;
inherit getAllObjects;
applyRootlessConfig =
prev: cfg:
let
isEnabled = supportRootless && prev.rootlessConfig.uid != null;
ifEnabled = lib.mkIf isEnabled;
userService = "user@${toString prev.rootlessConfig.uid}.service";
in
quadletUtils.unionOfDisjointRecursive cfg {
_rootless = isEnabled;
serviceConfig.User = ifEnabled prev.rootlessConfig.uid;
unitConfig.Wants = ifEnabled (lib.mkAfter [ "linger-users.service" ]);
unitConfig.Requires = ifEnabled (lib.mkAfter [ userService ]);
unitConfig.After = ifEnabled (
lib.mkAfter [
"linger-users.service"
userService
]
);
};
mkAssertions =
extraAssertions: config:
let
containerPodConflicts = lib.lists.intersectLists (lib.attrNames config.containers) (
lib.attrNames config.pods
);
nullImageArchiveTags = lib.attrNames (
lib.filterAttrs (
_: image:
lib.strings.hasPrefix "docker-archive:" image.imageConfig.image && image.imageConfig.tag == null
) config.images
);
in
[
{
assertion = containerPodConflicts == [ ];
message = ''
The container/pod names should be unique!
See: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#podname
The following names are not unique: ${lib.concatStringsSep " " containerPodConflicts}
'';
}
{
assertion = !(builtins.any (p: p._autoEscapeRequired) (getAllObjects config));
message = ''
`virtualisation.quadlet.autoEscape = true` is required because this configuration contains characters that require quoting or escaping.
If you have manual quoting or escaping in place, please undo those and enable `autoEscape`.
'';
}
{
assertion = nullImageArchiveTags == [ ];
message = ''
The following images using `docker-archive:` must have the fully qualified name (FQDN) specified as a tag: ${lib.concatStringsSep " " nullImageArchiveTags}
'';
}
]
++ extraAssertions;
mkWarnings =
extraWarnings: config:
(quadletUtils.assertionsToWarnings [
{
# TODO: drop string support and remove.
assertion =
!(builtins.any (p: builtins.isString p.networkConfig.options) (
builtins.attrValues config.networks
));
message = "String value in `virtualisation.quadlet.networks.*.networkConfig.options` is deprecated. Make it a list or attrset instead.";
}
])
++ extraWarnings;
};
in
self
================================================
FILE: pod.nix
================================================
{ quadletUtils, quadletOptions }:
{
config,
name,
lib,
...
}:
let
inherit (lib) types;
inherit (quadletUtils) encoders pkgs;
podOpts = {
name = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "name";
cli = "--name";
property = "PodName";
};
addHosts = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "hostname:192.168.10.11" ];
cli = "--add-host";
property = "AddHost";
};
modules = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/etc/nvd.conf" ];
cli = "--module";
property = "ContainersConfModule";
};
dns = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "192.168.55.1" ];
cli = "--dns";
property = "DNS";
};
dnsOptions = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "ndots:1" ];
cli = "--dns-option";
property = "DNSOption";
};
dnsSearches = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "foo.com" ];
cli = "--dns-search";
property = "DNSSearch";
};
exitPolicy = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "stop";
cli = "--exit-policy";
property = "ExitPolicy";
};
gidMaps = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "0:10000:10" ];
cli = "--gidmap";
property = "GIDMap";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
globalArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--log-level=debug" ];
description = "Additional command line arguments to insert between `podman` and `pod create`";
property = "GlobalArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
hostname = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "new-host-name";
cli = "--hostname";
property = "HostName";
};
ip = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "192.5.0.1";
cli = "--ip";
property = "IP";
};
ip6 = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "2001:db8::1";
cli = "--ip6";
property = "IP6";
};
labels = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
foo = "bar";
};
cli = "--label";
property = "Label";
encoders.scalar = encoders.scalar.quotedEscaped;
};
networks = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "host" ];
cli = "--network";
property = "Network";
};
networkAliases = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "name" ];
cli = "--network-alias";
property = "NetworkAlias";
};
podmanArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--cpus=2" ];
description = "Additional command line arguments to insert after `podman pod create`";
property = "PodmanArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
publishPorts = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "50-59" ];
cli = "--publish";
property = "PublishPort";
};
# ServiceName not supported as custom service names can make quadlet-nix lost.
shmSize = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "100m";
cli = "--shm-size";
property = "ShmSize";
};
stopTimeout = quadletOptions.mkOption {
type = types.nullOr types.int;
default = null;
example = 20;
cli = "--time";
property = "StopTimeout";
};
subGIDMap = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "gtest";
cli = "--subgidname";
property = "SubGIDMap";
};
subUIDMap = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "utest";
cli = "--subuidname";
property = "SubUIDMap";
};
uidMaps = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "0:10000:10" ];
cli = "--uidmap";
property = "UIDMap";
encoders.scalar = encoders.scalar.quotedUnescaped;
};
userns = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "keep-id:uid=200,gid=210";
cli = "--userns";
property = "UserNS";
};
volumes = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/source:/dest" ];
cli = "--volume";
property = "Volume";
};
};
in
{
options = quadletOptions.mkObjectOptions "pod" {
podConfig = podOpts;
};
config =
let
serviceConfigDefault = {
Restart = "always";
TimeoutStartSec = 900;
};
podName = if config.podConfig.name != null then config.podConfig.name else name;
podConfig = config.podConfig // {
name = podName;
};
quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
unitConfig = {
Unit = {
Description = "Podman pod ${name}";
}
// config.unitConfig;
Pod = quadletUtils.configToProperties podConfig podOpts;
Service = serviceConfigDefault // config.serviceConfig;
}
// (if quadlet == { } then { } else { Quadlet = quadlet; });
rootlessPidFilePath = "/run/user/${toString config.rootlessConfig.uid}/%N.pid";
in
lib.pipe
{
_serviceName = "${name}-pod";
_configText =
if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
_autoEscapeRequired = quadletUtils.autoEscapeRequired podConfig podOpts;
_autoStart = config.autoStart;
ref = "${name}.pod";
# [rootless hack]
# Quadlet manages pod infra container with PIDFile= at %t/%N.pid, which
# rootless process has no access to.
# Simply pointing PIDFile= at another location does not help as system
# systemd does not like PIDFIle= owned by unprivileged user while the
# process is out of the service.
# We therefore make it a Type=simple and wait for the pid in service.
podConfig.podmanArgs = lib.mkIf config._rootless (
lib.mkAfter [ "--infra-conmon-pidfile=${rootlessPidFilePath}" ]
);
serviceConfig.Type = lib.mkIf config._rootless (lib.mkDefault "simple");
serviceConfig.ExecStart = lib.mkIf config._rootless (
lib.mkDefault "/bin/sh -c \"${quadletUtils.podmanPackage}/bin/podman pod start ${podName} && (read pid < ${rootlessPidFilePath}; exec ${pkgs.coreutils}/bin/tail -f --pid \${pid:?})\""
);
serviceConfig.ExecStopPost = lib.mkIf config._rootless (
lib.mkAfter [
"${pkgs.coreutils}/bin/rm -f ${rootlessPidFilePath}"
]
);
# some options conflict with stock quadlet and thus need force overriding
_overrides =
quadletUtils.unionOfDisjointRecursive
(
# Type= as a singular field will be overwritten by Quadlet
if builtins.hasAttr "Type" config.serviceConfig then
{ serviceConfig.Type = config.serviceConfig.Type; }
else
{ }
)
(
# Type=simple does not support multiple ExecStart
if builtins.hasAttr "ExecStart" config.serviceConfig then
{
serviceConfig.ExecStart = [
""
config.serviceConfig.ExecStart
];
}
else
{ }
);
}
[
(quadletOptions.applyRootlessConfig config)
];
}
================================================
FILE: tests/README.md
================================================
# Tests
To run all tests:
```sh
nix flake check \
--override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \
--override-input home-manager 'github:nix-community/home-manager/master' \
--override-input test-config "path:$(pwd)/tests/x86_64-linux" \
./tests
```
To run individual test (e.g. `basic-rootful`):
```sh
nix run \
--override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \
--override-input home-manager 'github:nix-community/home-manager/master' \
--override-input test-config "path:$(pwd)/tests/x86_64-linux" \
'./tests#checks.x86_64-linux.basic-rootful.driver'
```
================================================
FILE: tests/aarch64-linux/flake.nix
================================================
{
outputs = _: {
system = "aarch64-linux";
};
}
================================================
FILE: tests/basic.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, ... }:
{
virtualisation.quadlet = {
containers.nginx = {
containerConfig.image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
containerConfig.publishPorts = [ "8080:80" ];
serviceConfig.TimeoutStartSec = "60";
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
html = machine.succeed("curl http://127.0.0.1:8080")
assert "nginx" in html.lower()
'';
}
================================================
FILE: tests/build.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, config, ... }:
{
virtualisation.quadlet =
let
inherit (config.virtualisation.quadlet) builds;
in
{
builds.hello = {
buildConfig = {
file = "${pkgs.writeText "Containerfile" ''
FROM docker-archive:${pkgs.dockerTools.examples.bash}
CMD bash -c 'echo "Success" > /output/result.txt'
''}";
};
}
// extraConfig;
containers.hello = {
containerConfig = {
image = builds.hello.ref;
volumes = [ "/tmp:/output" ];
};
serviceConfig = {
RemainAfterExit = true;
};
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("hello.service", user=systemd_user, timeout=30)
assert machine.succeed("cat /tmp/result.txt").strip() == 'Success'
'';
}
================================================
FILE: tests/container.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, ... }:
{
virtualisation.quadlet = {
containers.nginx = {
containerConfig.image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
containerConfig.publishPorts = [ "8080:80" ];
serviceConfig.TimeoutStartSec = "60";
serviceConfig.Restart = "on-failure";
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
assert 'nginx' in machine.succeed("curl http://127.0.0.1:8080").lower()
containers = get_containers()
assert containers.keys() == {"nginx"}
if podman_user is not None:
assert not get_containers(user=None)
machine.stop_job("nginx", user=systemd_user)
machine.fail("curl http://127.0.0.1:8080")
assert not get_containers()
machine.start_job("nginx", user=systemd_user)
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
assert 'nginx' in machine.succeed("curl http://127.0.0.1:8080").lower()
containers = get_containers()
assert containers.keys() == {"nginx"}
run_as("podman stop nginx", user=podman_user)
wait_for_unit_inactive("nginx.service", user=systemd_user, timeout=10)
'';
}
================================================
FILE: tests/escaping.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, ... }:
{
virtualisation.quadlet = {
containers.write1 = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.bash}";
# quotedUnescaped
addCapabilities = [ "SYS_NICE" ];
entrypoint = "bash";
# quotedEscaped
environments = {
FOO = "aaa bbb $ccc \"ddd\n\n ";
bar = "\"aaa\"";
ONLY_SPACES = "aaa bbb";
};
# raw
exec = "-c 'echo -n \"$FOO\" > /tmp/foo.txt; echo -n \"$bar\" > /tmp/bar.txt; echo -n \"$ONLY_SPACES\" > /tmp/only_spaces.txt'";
volumes = [
"/tmp:/tmp"
];
};
serviceConfig = {
RemainAfterExit = true;
};
}
// extraConfig;
containers.write2 = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.bash}";
environments = {
BAZ = "aaa";
};
entrypoint = "bash";
# oneLine
exec = [
"-c"
"echo $@ $0 $BAZ > /tmp/baz.txt"
"bbb"
"ccc"
];
volumes = [
"/tmp:/tmp"
];
};
serviceConfig = {
RemainAfterExit = true;
};
}
// extraConfig;
containers.write3 =
let
scriptName = "aaa bbb \n $ccc";
scriptDir = toString (pkgs.writeTextDir scriptName "echo 8439b333258ba90e > /tmp/write3.txt");
in
{
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.bash}";
entrypoint = [
"bash"
"/test/${scriptName}"
];
volumes = [
"/tmp:/tmp"
"${scriptDir}:/test/"
];
};
serviceConfig = {
RemainAfterExit = true;
};
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("write1.service", user=systemd_user, timeout=30)
machine.wait_for_unit("write2.service", user=systemd_user, timeout=30)
machine.wait_for_unit("write3.service", user=systemd_user, timeout=30)
machine.wait_for_file("/tmp/foo.txt", timeout=10)
assert machine.succeed("cat /tmp/foo.txt") == 'aaa bbb $ccc "ddd\n\n '
machine.wait_for_file("/tmp/bar.txt", timeout=10)
assert machine.succeed("cat /tmp/bar.txt") == '"aaa"'
machine.wait_for_file("/tmp/baz.txt", timeout=10)
assert machine.succeed("cat /tmp/baz.txt") == 'ccc bbb aaa\n'
machine.wait_for_file("/tmp/only_spaces.txt", timeout=10)
assert machine.succeed("cat /tmp/only_spaces.txt") == 'aaa bbb'
machine.wait_for_file("/tmp/write3.txt", timeout=10)
assert machine.succeed("cat /tmp/write3.txt") == '8439b333258ba90e\n'
'';
}
================================================
FILE: tests/flake.nix
================================================
# this is a separate flake to so home-manager isn't made a compulsory input.
{
description = "quadlet-nix tests";
# inputs path to be set in --override-input
inputs = {
nixpkgs.url = "path:/dev/null";
quadlet-nix.url = "path:..";
home-manager.url = "path:/dev/null";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
test-config.url = "path:/dev/null";
};
outputs =
{
test-config,
nixpkgs,
home-manager,
quadlet-nix,
...
}:
let
system = test-config.system;
makeTestScript =
{
podmanUser,
systemdUser,
testScript,
}:
{ nodes, ... }:
''
import json
from typing import Any, Optional
podman_user = ${podmanUser}
systemd_user = ${systemdUser}
def run_as(command: str, *, user: Optional[str]) -> str:
if user is not None:
command = f"sudo -u {user} -- {command}"
return machine.succeed(command)
def wait_for_unit_inactive(unit: str, *, user: Optional[str], timeout: int) -> None:
def check_active(_last_try: bool) -> bool:
state = machine.get_unit_property(unit, "ActiveState", user)
if state == "inactive":
return True
if state in ("active", "deactivating"):
return False
assert False, f"{unit} reached state {state}"
with machine.nested(
f"waiting for unit {unit}"
+ (f" with user {user}" if user is not None else "")
+ " to be inactive"
):
retry(check_active, timeout)
def get_containers(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:
containers = json.loads(run_as("podman ps --format=json", user=user))
return {name: container for container in containers for name in container["Names"]}
def get_networks(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:
networks = json.loads(run_as("podman network ls --format=json", user=user))
return {network["name"]: network for network in networks}
def get_pods(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:
pods = json.loads(run_as("podman pod ls --format=json", user=user))
return {pod["Name"]: pod for pod in pods}
def switch_to_specialisation(specialisation: str) -> str:
return machine.succeed(f"${nodes.machine.system.build.toplevel}/specialisation/{specialisation}/bin/switch-to-configuration test")
machine.wait_for_unit("default.target", user=None)
if systemd_user is not None:
machine.wait_for_unit("default.target", user=systemd_user)
${testScript}
'';
makeTestCase = template: args: if builtins.isFunction template then template args else template;
runRootfulTest =
{
name,
template,
pkgs,
}:
let
testCase = makeTestCase template {
extraConfig = { };
isHomeManager = false;
home = "/root";
};
testConfig = testCase.testConfig;
testScript = testCase.testScript;
specialisation = testCase.specialisation or (_: { });
in
{
name = name + "-rootful";
testScript = makeTestScript {
systemdUser = "None";
podmanUser = "None";
inherit testScript;
};
node.specialArgs.testType = "rootful";
nodes.machine =
{ pkgs, ... }@attrs:
{
imports = [
quadlet-nix.nixosModules.quadlet
testConfig
];
environment.systemPackages = [ pkgs.curl ];
specialisation = builtins.mapAttrs (name: value: { configuration = value; }) (specialisation attrs);
};
};
runRootlessTest =
{
name,
template,
pkgs,
}:
let
testCase = makeTestCase template {
extraConfig = {
rootlessConfig.uid = 1357;
};
isHomeManager = false;
home = "/home/alice";
};
testConfig = testCase.testConfig;
testScript = testCase.testScript;
specialisation = testCase.specialisation or (_: { });
in
{
name = name + "-rootless";
testScript = makeTestScript {
systemdUser = "None";
podmanUser = "\"alice\"";
inherit testScript;
};
node.specialArgs.testType = "rootless";
nodes.machine =
{ pkgs, ... }@attrs:
{
imports = [
quadlet-nix.nixosModules.quadlet
testConfig
];
environment.systemPackages = [ pkgs.curl ];
specialisation = builtins.mapAttrs (name: value: { configuration = value; }) (specialisation attrs);
users.users.alice = {
uid = 1357;
group = "alice";
linger = true;
autoSubUidGidRange = true;
isNormalUser = true;
};
users.groups.alice = {
gid = 2468;
};
};
};
runHomeManagerTest =
{
name,
template,
pkgs,
}:
let
testCase = makeTestCase template {
extraConfig = { };
isHomeManager = true;
home = "/home/alice";
};
testConfig = testCase.testConfig;
testScript = testCase.testScript;
specialisation = testCase.specialisation or (_: { });
in
{
name = name + "-home-manager";
testScript = makeTestScript {
systemdUser = "\"alice\"";
podmanUser = "\"alice\"";
inherit testScript;
};
nodes.machine =
{ lib, pkgs, ... }@attrs:
{
imports = [
quadlet-nix.nixosModules.quadlet
home-manager.nixosModules.home-manager
];
virtualisation.quadlet.enable = true;
environment.systemPackages = [ pkgs.curl ];
# brings up network-online.target
systemd.targets.test-network = {
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
};
users.users.alice = {
group = "alice";
linger = true;
autoSubUidGidRange = true;
isNormalUser = true;
};
users.groups.alice = { };
home-manager.extraSpecialArgs.testType = "home-manager";
home-manager.users.alice = lib.mkDefault (
{ config, ... }:
{
imports = [
quadlet-nix.homeManagerModules.quadlet
testConfig
];
home.stateVersion = config.home.version.release;
}
);
specialisation = builtins.mapAttrs (name: value: {
configuration = {
home-manager.users.alice = (
{ config, ... }:
{
imports = [
quadlet-nix.homeManagerModules.quadlet
testConfig
value
];
home.stateVersion = config.home.version.release;
}
);
};
}) (specialisation attrs);
};
};
genTest =
pkgs: runTest: template:
let
name = pkgs.lib.removeSuffix ".nix" (builtins.baseNameOf template);
test = pkgs.testers.runNixOSTest (runTest {
template = import template;
inherit name pkgs;
});
in
{
name = test.config.name;
value = test;
};
in
{
checks =
let
pkgs = import nixpkgs { inherit system; };
lib = pkgs.lib;
tests = builtins.listToAttrs (
map ({ runner, template }: genTest pkgs runner template) (
lib.cartesianProduct {
template = [
./basic.nix
./build.nix
./container.nix
./image.nix
./network.nix
./pod.nix
./volume.nix
./switch.nix
./raw.nix
./health.nix
./escaping.nix
./overriding.nix
];
runner = [
runRootfulTest
runRootlessTest
runHomeManagerTest
];
}
)
);
in
{
"${system}" = tests;
};
};
}
================================================
FILE: tests/health.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, ... }:
{
virtualisation.quadlet = {
containers.good = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.redis}";
healthCmd = "redis-cli ping || exit 1";
healthRetries = 1;
};
serviceConfig.TimeoutStartSec = 60;
}
// extraConfig;
containers.bad = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
healthCmd = "exit 1";
healthRetries = 1;
};
serviceConfig.TimeoutStartSec = 60;
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("good.service", user=systemd_user, timeout=30)
machine.wait_for_unit("bad.service", user=systemd_user, timeout=30)
machine.sleep(2) # wait for health command cycles
containers = get_containers()
assert containers.keys() == {"good", "bad"}
assert "(healthy)" in containers["good"]["Status"]
assert "(unhealthy)" in containers["bad"]["Status"]
'';
}
================================================
FILE: tests/image.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, config, ... }:
{
virtualisation.quadlet =
let
inherit (config.virtualisation.quadlet) images;
in
{
images.hello =
let
test-bash-image = pkgs.dockerTools.buildImage {
name = "whatever.com/test-bash";
tag = "latest";
fromImage = pkgs.dockerTools.examples.bash;
};
in
{
imageConfig = {
image = "docker-archive:${test-bash-image}";
tag = "whatever.com/test-bash:latest";
};
}
// extraConfig;
containers.hello = {
containerConfig = {
image = images.hello.ref;
volumes = [ "/tmp:/output" ];
entrypoint = "bash";
exec = [
"-c"
"echo \"Success\" > /output/result.txt"
];
};
serviceConfig = {
RemainAfterExit = true;
};
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("hello.service", user=systemd_user, timeout=30)
assert machine.succeed("cat /tmp/result.txt").strip() == 'Success'
'';
}
================================================
FILE: tests/network.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, config, ... }:
{
virtualisation.quadlet =
let
inherit (config.virtualisation.quadlet) networks;
in
{
containers.nginx = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
publishPorts = [ "8080:80" ];
networks = [
networks.foo.ref
networks.bar.ref
];
};
}
// extraConfig;
networks.foo = {
networkConfig.options.isolate = "true";
}
// extraConfig;
networks.bar = { } // extraConfig;
};
};
testScript = ''
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
containers = get_containers()
assert containers.keys() == {"nginx"}
networks = get_networks()
assert networks.keys() == {"foo", "bar", "podman"}
assert set(containers["nginx"]["Networks"]) == {"foo", "bar"}
assert networks["foo"]["options"]["isolate"] == "true"
if podman_user is not None:
assert not get_containers(user=None)
assert get_networks(user=None).keys() == {"podman"}
machine.stop_job("foo-network", user=systemd_user)
machine.fail("curl http://127.0.0.1:8080")
assert not get_containers()
networks = get_networks()
assert networks.keys() == {"bar", "podman"}
machine.start_job("nginx", user=systemd_user)
assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
containers = get_containers()
assert containers.keys() == {"nginx"}
networks = get_networks()
assert networks.keys() == {"foo", "bar", "podman"}
'';
}
================================================
FILE: tests/overriding.nix
================================================
{ extraConfig, isHomeManager, ... }:
{
testConfig =
{ pkgs, lib, ... }:
let
execStartPre = "${pkgs.bash}/bin/bash -c 'echo ef1e835e0ae5 > /tmp/foo.txt'";
nixosOverrides = {
systemd.services.nginx.serviceConfig.ExecStartPre = execStartPre;
};
homeManagerOverrides = {
systemd.user.services.nginx.Service.ExecStartPre = execStartPre;
};
overrides = if isHomeManager then homeManagerOverrides else nixosOverrides;
in
{
virtualisation.quadlet = {
containers.nginx = {
containerConfig.image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
containerConfig.publishPorts = [ "8080:80" ];
serviceConfig.TimeoutStartSec = "60";
}
// extraConfig;
};
}
// overrides;
testScript = ''
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
html = machine.succeed("curl http://127.0.0.1:8080")
assert "nginx" in html.lower()
assert machine.succeed("cat /tmp/foo.txt").strip() == "ef1e835e0ae5"
'';
}
================================================
FILE: tests/pod.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, config, ... }:
{
virtualisation.quadlet =
let
inherit (config.virtualisation.quadlet) pods;
in
{
containers.nginx = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
pod = pods.foo.ref;
};
serviceConfig.Restart = "on-failure";
}
// extraConfig;
containers.redis = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.redis}";
pod = pods.foo.ref;
};
serviceConfig.Restart = "on-failure";
}
// extraConfig;
pods.foo = {
podConfig = {
publishPorts = [ "8080:80" ];
};
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
machine.wait_for_unit("redis.service", user=systemd_user, timeout=30)
assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
containers = get_containers()
assert len(containers) == 3
assert containers.keys() >= {"nginx", "redis"}
pods = get_pods()
assert pods.keys() == {"foo"}
assert set(c["Id"] for c in pods["foo"]["Containers"]) == {c["Id"] for c in containers.values()}
if podman_user is not None:
assert not get_containers(user=None)
assert not get_pods(user=None)
machine.stop_job("foo-pod", user=systemd_user)
machine.fail("curl http://127.0.0.1:8080")
assert not get_containers()
assert not get_pods()
machine.start_job("nginx", user=systemd_user)
assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
machine.wait_for_unit("foo-pod.service", user=systemd_user, timeout=30)
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
machine.wait_for_unit("redis.service", user=systemd_user, timeout=30)
containers = get_containers()
assert len(containers) == 3
assert containers.keys() >= {"nginx", "redis"}
pods = get_pods()
assert pods.keys() == {"foo"}
run_as("podman pod stop foo", user=podman_user)
wait_for_unit_inactive("foo-pod.service", user=systemd_user, timeout=10)
wait_for_unit_inactive("nginx.service", user=systemd_user, timeout=10)
wait_for_unit_inactive("redis.service", user=systemd_user, timeout=10)
'';
}
================================================
FILE: tests/raw.nix
================================================
{ extraConfig, ... }:
{
testConfig =
{ pkgs, ... }:
{
virtualisation.quadlet = {
containers.nginx = {
rawConfig = ''
[Container]
Image=docker-archive:${pkgs.dockerTools.examples.nginx}
PublishPort=8080:80
[Service]
TimeoutStartSec=60
'';
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
html = machine.succeed("curl http://127.0.0.1:8080")
assert "nginx" in html.lower()
'';
}
================================================
FILE: tests/switch.nix
================================================
{ extraConfig, ... }:
let
makeQuadletConfig = pkgs: networks: {
containers.nginx = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
publishPorts = [ "8080:80" ];
networks = map (x: "${x}.network") networks;
};
}
// extraConfig;
networks = builtins.listToAttrs (
map (x: {
name = x;
value = {
networkConfig.name = x;
}
// extraConfig;
}) networks
);
};
in
{
testConfig =
{ lib, pkgs, ... }:
{
virtualisation.quadlet = lib.mkDefault (makeQuadletConfig pkgs [ "foo" ]);
};
specialisation =
{ pkgs, ... }:
{
step1Add.virtualisation.quadlet = makeQuadletConfig pkgs [
"foo"
"bar"
];
step2Remove.virtualisation.quadlet = makeQuadletConfig pkgs [ "bar" ];
step3AddRemove.virtualisation.quadlet = makeQuadletConfig pkgs [ "baz" ];
};
testScript = ''
def check(expected_networks: set[str]) -> None:
assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
containers = get_containers()
assert containers.keys() == {"nginx"}
networks = get_networks()
assert networks.keys() == expected_networks | {"podman"}
assert set(containers["nginx"]["Networks"]) == expected_networks
check({"foo"})
switch_to_specialisation("step1Add")
check({"foo", "bar"})
switch_to_specialisation("step2Remove")
check({"bar"})
switch_to_specialisation("step3AddRemove")
check({"baz"})
'';
}
================================================
FILE: tests/volume.nix
================================================
{ extraConfig, home, ... }:
{
testConfig =
{ pkgs, config, ... }:
{
virtualisation.quadlet =
let
inherit (config.virtualisation.quadlet) volumes;
in
{
containers.write = {
containerConfig = {
image = "docker-archive:${pkgs.dockerTools.examples.bash}";
entrypoint = "bash";
exec = "-c 'echo 262c837a9160 > /mnt/foo/bar.txt'";
volumes = [
"${volumes.foo.ref}:/mnt/foo"
];
};
serviceConfig = {
RemainAfterExit = true;
};
}
// extraConfig;
volumes.foo = {
volumeConfig = {
type = "bind";
device = home;
};
}
// extraConfig;
};
};
testScript = ''
machine.wait_for_unit("write.service", user=systemd_user, timeout=30)
path = "${home}/bar.txt"
machine.wait_for_file(path, timeout=10)
assert machine.succeed(f"cat {path}").strip() == "262c837a9160"
'';
}
================================================
FILE: tests/x86_64-linux/flake.nix
================================================
{
outputs = _: {
system = "x86_64-linux";
};
}
================================================
FILE: utils.nix
================================================
{
pkgs,
lib,
systemdUtils,
podmanPackage,
autoEscape,
}:
let
# encodes value based on how podman parses them
# see: https://github.com/containers/podman/blob/main/pkg/systemd/quadlet/quadlet.go
encoders =
let
# wraps a scalar encoder so it tries not escaping if possible
makePassive =
f: x:
let
raw = systemdUtils.lib.toOption x;
encoded = f x;
canSkip = encoded == raw || (builtins.match ".*[ \t\n\r].*" raw == null && "\"${raw}\"" == encoded);
in
if canSkip then raw else encoded;
in
{
scalar.legacy = systemdUtils.lib.toOption;
# Lookup, LookupAll, LookupLast, LookupAllRaw, LookupLastRaw
scalar.raw =
x:
let
ret = systemdUtils.lib.toOption x;
in
if builtins.match ".*[\r\n].*" ret == null then
ret
else
throw "quadlet-nix internal error: unsafe value for scalar.raw option: ${ret}";
# LookupAllArgs, LookupAllKeyVal
# same as systemdUtils.lib.serviceToUnit
scalar.quotedEscaped = makePassive builtins.toJSON;
# LookupAllStrv
scalar.quotedUnescaped = makePassive (
x:
let
escaped = builtins.toJSON x;
unescaped = "\"${systemdUtils.lib.toOption x}\"";
in
if escaped == unescaped then
unescaped
else
throw "quadlet-nix internal error: unsafe value for scalar.quotedUnescaped option: ${escaped}"
);
list.default = fScalar: x: map fScalar x;
# LookupLastArgs
list.oneLine = fScalar: x: builtins.concatStringsSep " " (map fScalar x);
list.json = builtins.toJSON;
attrs.default = fScalar: x: lib.mapAttrsToList (k: v: "${k}=${fScalar v}") x;
};
encode =
encoders: value:
if builtins.isString value || builtins.isInt value || builtins.isBool value then
encoders.scalar value
else if builtins.isList value then
encoders.list value
else if builtins.isAttrs value then
encoders.attrs value
else
throw "quadlet-nix internal error: unexpected type for encoder";
finalizeEncoders =
autoEscape: optionEncoders:
let
effEncoders = if autoEscape then optionEncoders else { scalar = encoders.scalar.legacy; };
scalar = effEncoders.scalar or encoders.scalar.raw;
list = effEncoders.list or (encoders.list.default scalar);
attrs = effEncoders.attrs or (encoders.attrs.default scalar);
in
{
inherit scalar list attrs;
};
configToProperties =
autoEscape: config: options:
let
nonNullConfig = lib.filterAttrs (_: value: value != null) config;
encodeEntry =
name: value:
lib.nameValuePair options.${name}.property (
encode (finalizeEncoders autoEscape options.${name}.encoders) value
);
in
lib.mapAttrs' encodeEntry nonNullConfig;
unionOfDisjointRecursive =
x: y:
let
intersectionY = builtins.intersectAttrs x y;
intersectionX = builtins.mapAttrs (n: _: x.${n}) intersectionY;
mergeFn =
name: values:
let
x = builtins.elemAt values 0;
y = builtins.elemAt values 1;
in
if builtins.isAttrs x && builtins.isAttrs y then
unionOfDisjointRecursive x y
else if x == y then
x
else
throw "unionOfDisjointRecursive: collision on ${name}";
merged = builtins.zipAttrsWith mergeFn [
intersectionX
intersectionY
];
in
x // y // merged;
in
{
configToProperties = config: options: configToProperties autoEscape config options;
autoEscapeRequired =
config: options:
configToProperties autoEscape config options != configToProperties true config options;
unitConfigToText =
unitConfig:
builtins.concatStringsSep "\n\n" (
lib.mapAttrsToList (
name: section: "[${name}]\n${systemdUtils.lib.attrsToSection section}"
) unitConfig
);
assertionsToWarnings =
asssertions: map (x: x.message) (builtins.filter (x: !x.assertion) asssertions);
inherit (systemdUtils.unitOptions) unitOption;
inherit
pkgs
podmanPackage
encoders
unionOfDisjointRecursive
;
}
================================================
FILE: volume.nix
================================================
{ quadletUtils, quadletOptions }:
{
config,
name,
lib,
...
}:
let
inherit (lib) types;
inherit (quadletUtils) encoders;
volumeOpts = {
name = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "foo";
description = "Volume name as in `podman volume create foo`";
property = "VolumeName";
};
copy = quadletOptions.mkOption {
type = types.nullOr types.bool;
default = null;
cli = "--opt copy";
property = "Copy";
};
device = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "tmpfs";
cli = "--opt device=...";
property = "Device";
};
driver = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "image";
cli = "--driver";
property = "Driver";
};
globalArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--log-level=debug" ];
description = "Additional command line arguments to insert between `podman` and `volume create`";
property = "GlobalArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
group = quadletOptions.mkOption {
type = types.nullOr (
types.oneOf [
types.int
types.str
]
);
default = null;
example = 192;
cli = "--opt group=...";
property = "Group";
};
image = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
example = "quay.io/centos/centos:latest";
cli = "--opt image=...";
property = "Image";
};
labels = quadletOptions.mkOption {
type = types.oneOf [
(types.listOf types.str)
(types.attrsOf types.str)
];
default = { };
example = {
foo = "bar";
};
cli = "--label";
property = "Label";
encoders.scalar = encoders.scalar.quotedEscaped;
};
modules = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "/etc/nvd.conf" ];
cli = "--module";
property = "ContainersConfModule";
};
options = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
cli = "--opt o=...";
property = "Options";
};
podmanArgs = quadletOptions.mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--driver=image" ];
description = "Additional command line arguments to insert after `podman volume create`";
property = "PodmanArgs";
encoders.scalar = encoders.scalar.quotedEscaped;
};
type = quadletOptions.mkOption {
type = types.nullOr types.str;
default = null;
cli = "--opt type=...";
description = "Filesystem type of `device`";
property = "Type";
};
user = quadletOptions.mkOption {
type = types.nullOr (
types.oneOf [
types.int
types.str
]
);
default = null;
example = 123;
cli = "--opt uid=...";
property = "User";
};
};
in
{
options = quadletOptions.mkObjectOptions "volume" {
volumeConfig = volumeOpts;
};
config =
let
volumeName = if config.volumeConfig.name != null then config.volumeConfig.name else name;
volumeConfig = config.volumeConfig // {
name = volumeName;
};
quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
unitConfig = {
Unit = {
Description = "Podman volume ${name}";
}
// config.unitConfig;
Volume = quadletUtils.configToProperties volumeConfig volumeOpts;
Service = config.serviceConfig;
}
// (if quadlet == { } then { } else { Quadlet = quadlet; });
in
lib.pipe
{
_serviceName = "${name}-volume";
_configText =
if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
_autoStart = config.autoStart;
_autoEscapeRequired = quadletUtils.autoEscapeRequired volumeConfig volumeOpts;
ref = "${name}.volume";
}
[
(quadletOptions.applyRootlessConfig config)
];
}
gitextract_vh7gx8rf/ ├── .github/ │ └── workflows/ │ └── test.yml ├── LICENSE ├── README.md ├── build.nix ├── container.nix ├── docs/ │ ├── README.md │ ├── flake.nix │ └── src/ │ └── SUMMARY.md ├── flake.nix ├── home-manager-module.nix ├── image.nix ├── network.nix ├── nixos-module.nix ├── options.nix ├── pod.nix ├── tests/ │ ├── README.md │ ├── aarch64-linux/ │ │ └── flake.nix │ ├── basic.nix │ ├── build.nix │ ├── container.nix │ ├── escaping.nix │ ├── flake.nix │ ├── health.nix │ ├── image.nix │ ├── network.nix │ ├── overriding.nix │ ├── pod.nix │ ├── raw.nix │ ├── switch.nix │ ├── volume.nix │ └── x86_64-linux/ │ └── flake.nix ├── utils.nix └── volume.nix
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (128K chars).
[
{
"path": ".github/workflows/test.yml",
"chars": 2549,
"preview": "name: test\n\non:\n push:\n pull_request:\n schedule:\n - cron: '0 16 * * *' # UTC 16:00 daily\n\njobs:\n format:\n runs-"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2023 SEIAROTg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 14377,
"preview": "# quadlet-nix\n\nManages Podman containers, networks, pods, etc. on NixOS via [Quadlet](https://docs.podman.io/en/latest/m"
},
{
"path": "build.nix",
"chars": 7371,
"preview": "{ quadletUtils, quadletOptions }:\n{\n config,\n name,\n lib,\n ...\n}:\nlet\n inherit (lib) types;\n inherit (quadletUtils"
},
{
"path": "container.nix",
"chars": 21271,
"preview": "{ quadletUtils, quadletOptions }:\n{\n config,\n name,\n lib,\n ...\n}:\nlet\n inherit (lib) types;\n inherit (quadletUtils"
},
{
"path": "docs/README.md",
"chars": 79,
"preview": "# Docs\n\nTo generate the documentation, run:\n\n```sh\nnix build './docs#book'\n```\n"
},
{
"path": "docs/flake.nix",
"chars": 2242,
"preview": "{\n description = \"quadlet-nix docs\";\n\n inputs = {\n nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n quadl"
},
{
"path": "docs/src/SUMMARY.md",
"chars": 138,
"preview": "# Contents\n\n- [Introduction](./introduction.md)\n- [NixOS Options](./nixos-options.md)\n- [Home Manager Options](./home-ma"
},
{
"path": "flake.nix",
"chars": 224,
"preview": "{\n description = \"NixOS and home-manager module for Podman Quadlets\";\n\n outputs =\n { self }:\n {\n nixosModul"
},
{
"path": "home-manager-module.nix",
"chars": 4535,
"preview": "{\n config,\n osConfig ? { },\n lib,\n pkgs,\n ...\n}:\nlet\n inherit (lib) mergeAttrsList mkIf getExe;\n\n cfg = config.vi"
},
{
"path": "image.nix",
"chars": 4865,
"preview": "{ quadletUtils, quadletOptions }:\n{\n config,\n name,\n lib,\n ...\n}:\nlet\n inherit (lib) types;\n inherit (quadletUtils"
},
{
"path": "network.nix",
"chars": 5627,
"preview": "{ quadletUtils, quadletOptions }:\n{\n config,\n name,\n lib,\n ...\n}:\nlet\n inherit (lib) types getExe;\n inherit (quadl"
},
{
"path": "nixos-module.nix",
"chars": 2573,
"preview": "{\n config,\n lib,\n pkgs,\n ...\n}:\nlet\n inherit (lib) mergeAttrsList mkIf;\n\n cfg = config.virtualisation.quadlet;\n q"
},
{
"path": "options.nix",
"chars": 9103,
"preview": "{\n lib,\n quadletUtils,\n supportRootless,\n}:\nlet\n mkOption =\n {\n property,\n cli ? null,\n descriptio"
},
{
"path": "pod.nix",
"chars": 8574,
"preview": "{ quadletUtils, quadletOptions }:\n{\n config,\n name,\n lib,\n ...\n}:\nlet\n inherit (lib) types;\n inherit (quadletUtils"
},
{
"path": "tests/README.md",
"chars": 616,
"preview": "# Tests\n\nTo run all tests:\n\n```sh\nnix flake check \\\n --override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \\"
},
{
"path": "tests/aarch64-linux/flake.nix",
"chars": 56,
"preview": "{\n outputs = _: {\n system = \"aarch64-linux\";\n };\n}\n"
},
{
"path": "tests/basic.nix",
"chars": 560,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, ... }:\n {\n virtualisation.quadlet = {\n containers.ngin"
},
{
"path": "tests/build.nix",
"chars": 991,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, config, ... }:\n {\n virtualisation.quadlet =\n let\n "
},
{
"path": "tests/container.nix",
"chars": 1274,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, ... }:\n {\n virtualisation.quadlet = {\n containers.ngin"
},
{
"path": "tests/escaping.nix",
"chars": 3035,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, ... }:\n {\n virtualisation.quadlet = {\n containers.writ"
},
{
"path": "tests/flake.nix",
"chars": 9261,
"preview": "# this is a separate flake to so home-manager isn't made a compulsory input.\n\n{\n description = \"quadlet-nix tests\";\n\n "
},
{
"path": "tests/health.nix",
"chars": 1129,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, ... }:\n {\n virtualisation.quadlet = {\n containers.good"
},
{
"path": "tests/image.nix",
"chars": 1316,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, config, ... }:\n {\n virtualisation.quadlet =\n let\n "
},
{
"path": "tests/network.nix",
"chars": 1818,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, config, ... }:\n {\n virtualisation.quadlet =\n let\n "
},
{
"path": "tests/overriding.nix",
"chars": 1072,
"preview": "{ extraConfig, isHomeManager, ... }:\n{\n testConfig =\n { pkgs, lib, ... }:\n let\n execStartPre = \"${pkgs.bash}"
},
{
"path": "tests/pod.nix",
"chars": 2504,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, config, ... }:\n {\n virtualisation.quadlet =\n let\n "
},
{
"path": "tests/raw.nix",
"chars": 585,
"preview": "{ extraConfig, ... }:\n{\n testConfig =\n { pkgs, ... }:\n {\n virtualisation.quadlet = {\n containers.ngin"
},
{
"path": "tests/switch.nix",
"chars": 1571,
"preview": "{ extraConfig, ... }:\nlet\n makeQuadletConfig = pkgs: networks: {\n containers.nginx = {\n containerConfig = {\n "
},
{
"path": "tests/volume.nix",
"chars": 1090,
"preview": "{ extraConfig, home, ... }:\n{\n testConfig =\n { pkgs, config, ... }:\n {\n virtualisation.quadlet =\n let"
},
{
"path": "tests/x86_64-linux/flake.nix",
"chars": 55,
"preview": "{\n outputs = _: {\n system = \"x86_64-linux\";\n };\n}\n"
},
{
"path": "utils.nix",
"chars": 4263,
"preview": "{\n pkgs,\n lib,\n systemdUtils,\n podmanPackage,\n autoEscape,\n}:\n\nlet\n # encodes value based on how podman parses the"
},
{
"path": "volume.nix",
"chars": 4337,
"preview": "{ quadletUtils, quadletOptions }:\n{\n config,\n name,\n lib,\n ...\n}:\nlet\n inherit (lib) types;\n inherit (quadletUtils"
}
]
About this extraction
This page contains the full source code of the SEIAROTg/quadlet-nix GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (117.3 KB), approximately 29.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.