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.
NixOS virtualisation.oci-containers - 👍 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.
arion - 👍 Supports Docker. - 😐 More indirection and moving parts. - 👎 Limited options. - 👎 Incompatible with podman auto-update.
Vanilla Podman Quadlet - 👍 Even less indirection. - 😐 Compatible with podman auto-update (requires external setup). - 😐 Requires more work to set up. - 👎 Not integrated with rest of Nix configuration.
Home Manager services.podman - 👍 Part of Home Manager, no additional dependencies if you are already using it. - 👎 Lack of rootful container support.
compose2nix - 👍 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.
## How See [seiarotg.github.io/quadlet-nix](https://seiarotg.github.io/quadlet-nix) for all options. ## Recipes
Rootful containers #### `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 = { }; }; }; } ```
Rootless containers (via Home Manager) #### `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"; }; }; }; }; } ```
Rootless containers (in system systemd) ⚠️ 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"; }; } ```
Volumes ```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"; }; }; } ```
Build (inlined Containerfile) ```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; }; } ```
Build (git repository) ```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`.
Image ```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"; }; } ```
Install raw Quadlet files 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 = { }; }; }; } ```
Work with pkgs.dockerTools 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
Podman DNS not working? 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 ]; # ... } ```
Dependencies 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" ]; }; }; }; } ```
Debug & log access `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 ` and `journalctl -u `, where `` is container name, `-network`, `-pod`, or similar. These names are the names as appeared in `virtualisation.quadlet.containers.`, rather than podman container name, in case it's different.
The option I need is not available 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.
================================================ 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.. 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) ]; }