Full Code of SEIAROTg/quadlet-nix for AI

main 5e7016827231 cached
33 files
117.3 KB
29.5k tokens
1 requests
Download .txt
Repository: SEIAROTg/quadlet-nix
Branch: main
Commit: 5e7016827231
Files: 33
Total size: 117.3 KB

Directory structure:
gitextract_vh7gx8rf/

├── .github/
│   └── workflows/
│       └── test.yml
├── LICENSE
├── README.md
├── build.nix
├── container.nix
├── docs/
│   ├── README.md
│   ├── flake.nix
│   └── src/
│       └── SUMMARY.md
├── flake.nix
├── home-manager-module.nix
├── image.nix
├── network.nix
├── nixos-module.nix
├── options.nix
├── pod.nix
├── tests/
│   ├── README.md
│   ├── aarch64-linux/
│   │   └── flake.nix
│   ├── basic.nix
│   ├── build.nix
│   ├── container.nix
│   ├── escaping.nix
│   ├── flake.nix
│   ├── health.nix
│   ├── image.nix
│   ├── network.nix
│   ├── overriding.nix
│   ├── pod.nix
│   ├── raw.nix
│   ├── switch.nix
│   ├── volume.nix
│   └── x86_64-linux/
│       └── flake.nix
├── utils.nix
└── volume.nix

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: test

on:
  push:
  pull_request:
  schedule:
  - cron: '0 16 * * *'  # UTC 16:00 daily

jobs:
  format:
    runs-on: ubuntu-latest

    steps:
      - name: checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: DeterminateSystems/determinate-nix-action@v3
        with:
          extra-conf: |
            lazy-trees = true
            eval-cores = 0
      - run: nix run nixpkgs#nixfmt-tree -- --ci

  test:
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        # TODO: re-enable aarch64-linux once github native runner supports nested virtualization.
        system: [x86_64-linux]
        version:
        - nixpkgs: nixos-25.11
          home-manager: release-25.11
        - nixpkgs: nixos-unstable
          home-manager: master

    steps:
    - name: checkout
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - uses: DeterminateSystems/determinate-nix-action@v3
      with:
        extra-conf: |
          lazy-trees = true
          eval-cores = 0

    - uses: endersonmenezes/free-disk-space@v3
      with:
        remove_android: true
        remove_dotnet: true
        remove_haskell: true
        rm_cmd: rmz
        rmz_version: 3.1.1

    - uses: cachix/cachix-action@v16
      env:
        CACHIX_AUTH_TOKEN_PRESENT: ${{ secrets.CACHIX_AUTH_TOKEN != '' }}
      if: ${{ env.CACHIX_AUTH_TOKEN_PRESENT == 'true' }}
      with:
        name: quadlet-nix
        authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

    - name: Run tests
      run: >
        nix flake check
        --keep-going
        --all-systems
        --override-input nixpkgs 'github:NixOS/nixpkgs/${{ matrix.version.nixpkgs }}'
        --override-input home-manager 'github:nix-community/home-manager/${{ matrix.version.home-manager }}'
        --override-input test-config "path:$(pwd)/tests/${{ matrix.system }}"
        ./tests

    - name: Build docs
      run: |
        nix build ./docs#book

    - name: Upload docs
      if: matrix.system == 'x86_64-linux' && matrix.version.nixpkgs == 'nixos-unstable'
      uses: actions/upload-pages-artifact@v3
      with:
        path: ./result

  pass:
    needs: [format, test]
    runs-on: ubuntu-slim
    steps:
    - run: true

  publish-docs:
    if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
    needs: pass
    runs-on: ubuntu-latest
    permissions:
      pages: write
      id-token: write
    steps:
    - uses: actions/deploy-pages@v4


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 SEIAROTg

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# quadlet-nix

Manages Podman containers, networks, pods, etc. on NixOS via [Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html).

## Features

- Supports Podman containers, networks, pods, volumes, etc.
- Supports declarative update and deletion of networks.
- Supports rootful and rootless (via [Home Manager](https://github.com/nix-community/home-manager)) resources behind the same interface.
- Supports [Podman auto-update][podman-auto-update].
- Supports cross-referencing between resources in Nix language.
- Full quadlet options support, typed and properly escaped.
- Reliability through effective testing.
- Simplicity.
- Whatever offered by Nix or Quadlet.

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

## Motivation

This project was started in Aug 2023, as a result of [the author's frustration on some relatively simple container management needs](https://seiarotg.me/post/tidy-up-homelab-containers/), where then available technologies are either overly restrictive, or overly complex that requires non-trivial but pointless investment ad-hoc domain knowledge.

`quadlet-nix` is designed to be a simple tool that just works. Quadlet options are directly mapped into Nix, allowing users to effectively manage their Podman resources in the Nix language, without having to acquire domain knowledge in yet another tool. Prior knowledge and documentation of Podman continue to apply.

## Comparison

Below are comparisons with several alternatives for declaratively managing Podman containers on NixOS, effective as of May 2025.

<details>
<summary><a href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/oci-containers.nix" target="_blank">NixOS <code>virtualisation.oci-containers</code></a></summary>

- 👍 Part of NixOS, no additional dependencies.
- 👍 Rootless container support without additional dependencies.
- 👍 Supports Docker.
- 😐 Compatible with podman auto-update (requires external setup).
- 👎 Limited options.
- 👎 Lack of support for networks, pods, etc.

</details>

<details>
<summary><a href="https://github.com/hercules-ci/arion" target="_blank"><code>arion</code></a></summary>

- 👍 Supports Docker.
- 😐 More indirection and moving parts.
- 👎 Limited options.
- 👎 Incompatible with podman auto-update.

</details>

<details>
<summary><a href="https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html"  target="_blank">Vanilla Podman Quadlet</a></summary>

- 👍 Even less indirection.
- 😐 Compatible with podman auto-update (requires external setup).
- 😐 Requires more work to set up.
- 👎 Not integrated with rest of Nix configuration.

</details>

<details>
<summary><a href="https://nix-community.github.io/home-manager/options.xhtml#opt-services.podman.enable" target="_blank">Home Manager <code>services.podman</code></a></summary>

- 👍 Part of Home Manager, no additional dependencies if you are already using it.
- 👎 Lack of rootful container support.

</details>

<details>
<summary><a href="https://github.com/aksiksi/compose2nix" target="_blank"><code>compose2nix</code></a></summary>

- 👍 Supports Docker.
- 😐 Compatible with podman auto-update (requires external setup).
- 😐 More indirection and moving parts.
- 👎 Less maintainable Nix files due to generated boilerplate.
- 👎 Manual regeneration is required.
- 👎 Lack of rootless container support.
- 👎 Limited options.
- 👎 Fragmented configuration with source of truth being outside of Nix.

</details>

## How

See [seiarotg.github.io/quadlet-nix](https://seiarotg.github.io/quadlet-nix) for all options.

## Recipes

<details open>
<summary>Rootful containers</summary>

#### `flake.nix`

```nix
{
    inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
        quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
    };
    outputs = { nixpkgs, quadlet-nix, ... }@attrs: {
        nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
            system = "x86_64-linux";
            modules = [
                ./configuration.nix
                quadlet-nix.nixosModules.quadlet
            ];
        };
    };
}
```

#### `configuration.nix`

```nix
{ config, ... }: {
    # ...
    virtualisation.quadlet = let
        inherit (config.virtualisation.quadlet) networks pods;
    in {
        containers = {
            nginx.containerConfig.image = "docker.io/library/nginx:latest";
            nginx.containerConfig.networks = [ "podman" networks.internal.ref ];
            nginx.containerConfig.pod = pods.foo.ref;
            nginx.serviceConfig.TimeoutStartSec = "60";
        };
        networks = {
            internal.networkConfig.subnets = [ "10.0.123.1/24" ];
        };
        pods = {
            foo = { };
        };
    };
}
```

</details>

<details>
<summary>Rootless containers (via Home Manager)</summary>

#### `flake.nix`

```nix
{
    inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
        home-manager.url = "github:nix-community/home-manager";
        home-manager.inputs.nixpkgs.follows = "nixpkgs";
        quadlet-nix.url = "github:SEIAROTg/quadlet-nix";
    };
    outputs = { nixpkgs, quadlet-nix, home-manager, ... }@attrs: {
        nixosConfigurations.machine = nixpkgs.lib.nixosSystem {
            system = "x86_64-linux";
            modules = [
                ./configuration.nix
                home-manager.nixosModules.home-manager
                # to enable podman & podman systemd generator
                quadlet-nix.nixosModules.quadlet
            ];
        };
    };
}
```

#### `configuration.nix`

```nix
{
    # ...
    # to enable podman & podman systemd generator
    virtualisation.quadlet.enable = true;
    users.users.alice = {
        # ...
        # required for auto start before user login
        linger = true;
        # required for rootless container with multiple users
        autoSubUidGidRange = true;
    };
    home-manager.users.alice = { pkgs, config, ... }: {
        # ...
        imports = [ inputs.quadlet-nix.homeManagerModules.quadlet ];
        virtualisation.quadlet.containers = {
            echo-server = {
                autoStart = true;
                serviceConfig = {
                    RestartSec = "10";
                    Restart = "always";
                };
                containerConfig = {
                    image = "docker.io/mendhak/http-https-echo:31";
                    publishPorts = [ "127.0.0.1:8080:8080" ];
                    userns = "keep-id";
                };
            };
        };
    };
}
```

</details>

<details>
<summary>Rootless containers (in system systemd)</summary>

⚠️ Not officially supported by Podman. Use at your own risk and expect breaking changes.

```nix
{ config, ... }: {
    users.users.alice = {
        uid = 1234;
        # required for auto start before user login
        linger = true;
        # required for rootless container with multiple users
        autoSubUidGidRange = true;
    };
    virtualisation.quadlet.containers.nginx = {
        rootlessConfig.uid = config.users.users.alice.uid;
        containerConfig.image = "docker.io/library/nginx:latest";
    };
}
```

</details>

<details>
<summary>Volumes</summary>

```nix
{ config, ... }: {
    # ...
    virtualisation.quadlet = let
        inherit (config.virtualisation.quadlet) volumes;
    in {
        containers.nginx.containerConfig.image = "docker.io/library/nginx:latest";
        containers.nginx.containerConfig.volumes = [
            "${volumes.nginx-config.ref}:/etc/nginx"
        ];
        volumes.nginx-config.volumeConfig = {
            type = "bind";
            device = "/path/to/host/directory";
        };
    };
}
```

</details>

<details>
<summary>Build (inlined <code>Containerfile</code>)</summary>

```nix
{ pkgs, config, ... }: {
    # ...
    virtualisation.quadlet = let
        inherit (config.virtualisation.quadlet) builds;
        containerfile = pkgs.writeText "Containerfile" ''
          FROM docker.io/library/nginx:latest
          # ...
        '';
    in {
        containers.nginx.containerConfig.image = builds.nginx.ref;
        builds.nginx.buildConfig.file = containerfile.outPath;
    };
}
```

</details>

<details>
<summary>Build (git repository)</summary>

```nix
{ config, ... }: {
    # ...
    virtualisation.quadlet = let
        inherit (config.virtualisation.quadlet) builds;
        src = builtins.fetchGit {
          url = "https://github.com/alpinelinux/docker-alpine.git";
          rev = "4dc13cbc7caffe03c98aa99f28e27c2fb6f7e74d";
        };
    in {
        containers.example.containerConfig = {
          image = builds.alpine.ref;
          entrypoint = "/bin/sh";
          exec = "-c 'echo 123'";
        };
        containers.example.serviceConfig.RemainAfterExit = true;
        builds.alpine.buildConfig = {
          tag = "alpine:3.22";
          workdir = "${src}/x86_64";
        };
    };
}
```

Alternatively, git integration of Podman can be used through `workdir = "https://github.com/nginx/docker-nginx.git"`. However, it will be users' responsibility to make binaries such as `git` available to the build service via `PATH`.

</details>

<details>
<summary>Image</summary>

```nix
{ config, ... }: {
    # ...
    virtualisation.quadlet = let
        inherit (config.virtualisation.quadlet) images;
    in {
        containers.nginx.containerConfig.image = images.nginx.ref;
        images.nginx.imageConfig.image = "docker-archive:/path/to/local/image";
        images.nginx.imageConfig.tag = "docker.com/library/nginx:latest";
    };
}
```

</details>

<details>
<summary>Install raw Quadlet files</summary>

If you wish to write raw Quadlet files instead of using the Nix options, you may do so with `rawConfig`. Using this will cause all other options (except `autoStart`) to be ignored though.

```nix
{ config, ... }: {
    # ...
    virtualisation.quadlet = let
        inherit (config.virtualisation.quadlet) networks pods;
    in {
        containers = {
            nginx.rawConfig = ''
                [Container]
                Image=docker.io/library/nginx:latest
                Network=podman
                Network=${networks.internal.ref}
                Pod=${pods.foo.ref}
                [Service]
                TimeoutStartSec=60
            '';
        };
        networks = {
            internal.networkConfig.subnets = [ "10.0.123.1/24" ];
        };
        pods = {
            foo = { };
        };
    };
}
```
</details>

<details>
<summary>Work with <code>pkgs.dockerTools</code></summary>

Podman natively supports multiple transport, including `docker-archive` that can be used with `pkgs.dockerTools`.

```nix
{ pkgs, ... }: let
    image = pkgs.dockerTools.buildImage {
        # ...
    };
in {
    virtualisation.quadlet.containers = {
        foo.containerConfig.image = "docker-archive:${image}";
    };
}
```

See: https://docs.podman.io/en/v5.5.0/markdown/podman-run.1.html#image

</details>

<details>
<summary>Podman DNS not working?</summary>

To use Podman DNS, it needs to be enabled and allowed by your firewall.

For the default network, below sets up both for you:

```nix
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
```

Or if you manage firewall separately, allow UDP port 53 on the input chain on host interface "podman0" and set:

```nix
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
virtualisation.podman.defaultNetwork.settings.network_interface = "podman0";
```

For custom networks managed by Quadlet, Podman DNS is enabled by default, unless `disableDns` is set. To set up the firewall rules:

```nix
virtualisation.quadlet.networks.foo.networkConfig.interfaceName = "br-foo";
networking.firewall.interfaces.br-foo.allowedUDPPorts = [ 53 ];
```

To apply this on all custom networks:

#### `enable-dns.nix`

```nix
{ config, lib, ... }: {
  options.virtualisation.quadlet.networks = lib.mkOption {
    type = lib.types.attrsOf (lib.types.submodule ( { name, ... }: {
      networkConfig.driver = "bridge";
      networkConfig.interfaceName = "br-${name}";
    }));
  };
  config.networking.firewall.interfaces = lib.mapAttrs' (name: _: {
    name = "br-${name}";
    value.allowedUDPPorts = [ 53 ];
  }) config.virtualisation.quadlet.networks;
}
```

#### `configuration.nix`

```nix
{
  imports = [
    ./enable-dns.nix
  ];
  # ...
}
```

</details>

<details>
<summary>Dependencies</summary>

Obvious dependencies such as those between containers and their networks are automatically set up by Quadlet, and thus no additional configuration is needed.

Extra dependencies can be set up in systemd unit config. Note that `.ref` syntax is only valid in quadlet and does not work from regular systemd units.

```nix
{ config, ... }: {
    # ...
    virtualisation.quadlet = let
        inherit (config.virtualisation.quadlet) containers;
    in {
        containers = {
            database = {
                # ...
            };
            server = {
               # ...
               unitConfig.Requires = [ containers.database.ref "network-online.target" ];
               unitConfig.After = [ containers.database.ref "network-online.target" ];
            };
        };
    };
}
```

</details>

<details>
<summary>Debug & log access</summary>

`quadlet-nix` tries to put containers into full management under systemd. This means once a container crashes, it will be fully deleted and debugging mechanisms like `podman ps -a` or `podman logs` will not work.

However, status and logs are still accessible through systemd, namely, `systemctl status <service name>` and `journalctl -u <service name>`, where `<service name>` is container name, `<network name>-network`, `<pod name>-pod`, or similar. These names are the names as appeared in `virtualisation.quadlet.containers.<container name>`, rather than podman container name, in case it's different.

</details>

<details>
<summary>The option I need is not available</summary>

Check if that option is supported by Podman Quadlet here: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html.

If it exists, please create an issue or send a PR to add.

Otherwise, please use `PodmanArgs` and `GlobalArgs` to insert additional command line arguments as `quadlet-nix` does not intend to support options beyond what Quadlet offers.

</details>


================================================
FILE: build.nix
================================================
{ quadletUtils, quadletOptions }:
{
  config,
  name,
  lib,
  ...
}:
let
  inherit (lib) types;
  inherit (quadletUtils) encoders;

  buildOpts = {
    annotations = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        annotation = "value";
      };
      cli = "--annotation";
      property = "Annotation";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    arch = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "aarch64";
      cli = "--arch";
      property = "Arch";
    };

    authFile = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/etc/registry/auth.json";
      cli = "--authfile";
      property = "AuthFile";
    };

    buildArgs = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--build-arg";
      property = "BuildArg";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    modules = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/etc/nvd.conf" ];
      cli = "--module";
      property = "ContainersConfModule";
    };

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

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

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

    environments = quadletOptions.mkOption {
      type = types.attrsOf types.str;
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--env";
      property = "Environment";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    file = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/path/to/Containerfile";
      cli = "--file";
      property = "File";
    };

    forceRm = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--force-rm";
      property = "ForceRM";
    };

    globalArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--log-level=debug" ];
      description = "Additional command line arguments to insert between `podman` and `build`";
      property = "GlobalArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    addGroups = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "keep-groups" ];
      cli = "--group-add";
      property = "GroupAdd";
    };

    ignoreFile = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/path/to/.customignore";
      cli = "--ignorefile";
      property = "IgnoreFile";
    };

    tag = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "localhost/imagename";
      cli = "--tag";
      property = "ImageTag";
    };

    labels = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--label";
      property = "Label";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    networks = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "host" ];
      cli = "--net";
      property = "Network";
    };

    podmanArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--add-host foobar" ];
      description = "Additional command line arguments to insert after `podman build`";
      property = "PodmanArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    pull = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "never";
      cli = "--pull";
      property = "Pull";
    };

    retry = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 5;
      cli = "--retry";
      property = "Retry";
    };

    retryDelay = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "5s";
      cli = "--retry-delay";
      property = "RetryDelay";
    };

    secrets = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "secret[,opt=opt …]" ];
      cli = "--secret";
      property = "Secret";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    workdir = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "file";
      description = "Sets WorkingDirectory of systemd unit file";
      property = "SetWorkingDirectory";
    };

    target = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "my-app";
      cli = "--target";
      property = "Target";
    };

    tlsVerify = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--tls-verify";
      property = "TLSVerify";
    };

    variant = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "arm/v7";
      cli = "--variant";
      property = "Variant";
    };

    volumes = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/source:/dest" ];
      cli = "--volume";
      property = "Volume";
    };
  };

  serviceConfigDefault = {
    TimeoutStartSec = 900;
  };
in
{
  options = quadletOptions.mkObjectOptions "build" {
    buildConfig = buildOpts;
  };

  config =
    let
      buildTag = if config.buildConfig.tag != null then config.buildConfig.tag else "localhost/${name}";
      buildConfig = config.buildConfig // {
        tag = buildTag;
      };
      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
      unitConfig = {
        Unit = {
          Description = "Podman build ${name}";
        }
        // config.unitConfig;
        Build = quadletUtils.configToProperties buildConfig buildOpts;
        Service = serviceConfigDefault // config.serviceConfig;
      }
      // (if quadlet == { } then { } else { Quadlet = quadlet; });
    in
    lib.pipe
      {
        _serviceName = "${name}-build";
        _configText =
          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
        _autoStart = config.autoStart;
        _autoEscapeRequired = quadletUtils.autoEscapeRequired buildConfig buildOpts;
        ref = "${name}.build";
      }
      [
        (quadletOptions.applyRootlessConfig config)
      ];
}


================================================
FILE: container.nix
================================================
{ quadletUtils, quadletOptions }:
{
  config,
  name,
  lib,
  ...
}:
let
  inherit (lib) types;
  inherit (quadletUtils) encoders;

  containerOpts = {
    addCapabilities = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "NET_ADMIN" ];
      cli = "--cap-add";
      property = "AddCapability";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    addHosts = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "hostname:192.168.10.11" ];
      cli = "--add-host";
      property = "AddHost";
    };

    devices = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/dev/foo" ];
      cli = "--device";
      property = "AddDevice";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    annotations = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        annotation = "value";
      };
      cli = "--annotation";
      property = "Annotation";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    autoUpdate = quadletOptions.mkOption {
      type = types.nullOr (
        types.enum [
          "registry"
          "local"
        ]
      );
      default = null;
      example = "registry";
      cli = "--label \"io.containers.autoupdate=...\"";
      property = "AutoUpdate";
    };

    appArmor = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "alternate-profile";
      cli = "--security-opt apparmor=...";
      property = "AppArmor";
    };

    cgroupsMode = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "no-conmon";
      cli = "--cgroups";
      property = "CgroupsMode";
    };

    name = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "name";
      cli = "--name";
      property = "ContainerName";
    };

    modules = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/etc/nvd.conf" ];
      cli = "--module";
      property = "ContainersConfModule";
    };

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

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

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

    dropCapabilities = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "NET_ADMIN" ];
      cli = "--cap-drop";
      property = "DropCapability";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    entrypoint = quadletOptions.mkOption {
      type = types.nullOr (
        types.oneOf [
          types.str
          (types.listOf types.str)
        ]
      );
      default = null;
      example = "/foo.sh";
      cli = "--entrypoint";
      property = "Entrypoint";
      encoders.raw = encoders.scalar.raw;
      encoders.list = encoders.list.json;
    };

    environments = quadletOptions.mkOption {
      type = types.attrsOf types.str;
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--env";
      property = "Environment";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    environmentFiles = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/tmp/env" ];
      cli = "--env-file";
      property = "EnvironmentFile";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    environmentHost = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--env-host";
      property = "EnvironmentHost";
    };

    exec = quadletOptions.mkOption {
      type = types.nullOr (
        types.oneOf [
          types.str
          (types.listOf types.str)
        ]
      );
      default = null;
      example = "/usr/bin/command";
      description = "Command after image specification";
      property = "Exec";
      # CAVEAT: doesn't prevent systemd environment variable substitution, but probably a quadlet problem?
      encoders.scalar = encoders.scalar.raw;
      encoders.list = encoders.list.oneLine encoders.scalar.quotedEscaped;
    };

    exposePorts = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "50-59" ];
      cli = "--expose";
      property = "ExposeHostPort";
    };

    gidMaps = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "0:10000:10" ];
      cli = "--gidmap";
      property = "GIDMap";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    globalArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--log-level=debug" ];
      description = "Additional command line arguments to insert between `podman` and `run`";
      property = "GlobalArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    group = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "1234";
      cli = "--user UID:...";
      property = "Group";
    };

    addGroups = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "keep-groups" ];
      cli = "--group-add";
      property = "GroupAdd";
    };

    healthCmd = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/usr/bin/command";
      cli = "--health-cmd";
      property = "HealthCmd";
    };

    healthInterval = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "2m";
      cli = "--health-interval";
      property = "HealthInterval";
    };

    healthLogDestination = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/foo/log";
      cli = "--health-log-destination";
      property = "HealthLogDestination";
    };

    healthMaxLogCount = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 5;
      cli = "--health-max-log-count";
      property = "HealthMaxLogCount";
    };

    healthMaxLogSize = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 500;
      cli = "--health-max-log-size";
      property = "HealthMaxLogSize";
    };

    healthOnFailure = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "kill";
      cli = "--health-on-failure";
      property = "HealthOnFailure";
    };

    healthRetries = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 5;
      cli = "--health-retries";
      property = "HealthRetries";
    };

    healthStartPeriod = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "1m";
      cli = "--health-start-period";
      property = "HealthStartPeriod";
    };

    healthStartupCmd = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/usr/bin/command";
      cli = "--health-startup-cmd";
      property = "HealthStartupCmd";
    };

    healthStartupInterval = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "1m";
      cli = "--health-startup-interval";
      property = "HealthStartupInterval";
    };

    healthStartupRetries = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 8;
      cli = "--health-startup-retries";
      property = "HealthStartupRetries";
    };

    healthStartupSuccess = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 2;
      cli = "--health-startup-success";
      property = "HealthStartupSuccess";
    };

    healthStartupTimeout = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "1m33s";
      cli = "--health-startup-timeout";
      property = "HealthStartupTimeout";
    };

    healthTimeout = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "20s";
      cli = "--health-timeout";
      property = "HealthTimeout";
    };

    hostname = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "new-host-name";
      cli = "--hostname";
      property = "HostName";
    };

    httpProxy = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--http-proxy";
      property = "HttpProxy";
    };

    image = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "docker.io/library/nginx:latest";
      description = "Image specification";
      property = "Image";
    };

    ip = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "192.5.0.1";
      cli = "--ip";
      property = "IP";
    };

    ip6 = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "fd46:db93:aa76:ac37::10";
      cli = "--ip6";
      property = "IP6";
    };

    labels = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--label";
      property = "Label";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    logDriver = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "journald";
      cli = "--log-driver";
      property = "LogDriver";
    };

    logOptions = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "path=/var/log/mykube.json" ];
      cli = "--log-opt";
      property = "LogOpt";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    mask = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/proc/sys/foo:/proc/sys/bar";
      cli = "--security-opt mask=...";
      property = "Mask";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    memory = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "20g";
      cli = "--memory";
      property = "Memory";
    };

    mounts = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "type=..." ];
      cli = "--mount";
      property = "Mount";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    networks = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "host" ];
      cli = "--net";
      property = "Network";
    };

    networkAliases = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "name" ];
      cli = "--network-alias";
      property = "NetworkAlias";
    };

    noNewPrivileges = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--security-opt no-new-privileges";
      property = "NoNewPrivileges";
    };

    notify = quadletOptions.mkOption {
      type = types.enum [
        null
        true
        false
        "healthy"
      ];
      default = null;
      cli = "--sdnotify container";
      property = "Notify";
    };

    pidsLimit = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 10000;
      cli = "--pids-limit";
      property = "PidsLimit";
    };

    pod = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      cli = "--pod";
      property = "Pod";
    };

    podmanArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--add-host foobar" ];
      description = "Additional command line arguments to insert after `podman run`";
      property = "PodmanArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    publishPorts = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "50-59" ];
      cli = "--publish";
      property = "PublishPort";
    };

    pull = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "never";
      cli = "--pull";
      property = "Pull";
    };

    readOnly = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--read-only";
      property = "ReadOnly";
    };

    readOnlyTmpfs = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--read-only-tmpfs";
      property = "ReadOnlyTmpfs";
    };

    reloadCmd = quadletOptions.mkOption {
      type = types.nullOr (
        types.oneOf [
          types.str
          (types.listOf types.str)
        ]
      );
      default = null;
      description = "Adds ExecReload and run exec with the value";
      example = "/usr/bin/command";
      property = "ReloadCmd";
      encoders.scalar = encoders.scalar.raw;
      encoders.list = encoders.list.oneLine encoders.scalar.quotedEscaped;
    };

    reloadSignal = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      description = "Add ExecReload and run kill with the signal";
      example = "SIGHUP";
      property = "ReloadSignal";
    };

    retry = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 5;
      cli = "--retry";
      property = "Retry";
    };

    retryDelay = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "5s";
      cli = "--retry-delay";
      property = "RetryDelay";
    };

    rootfs = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/var/lib/rootfs";
      cli = "--rootfs";
      property = "Rootfs";
    };

    runInit = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--init";
      property = "RunInit";
    };

    seccompProfile = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/tmp/s.json";
      cli = "--security-opt seccomp=...";
      property = "SeccompProfile";
    };

    secrets = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "secret[,opt=opt …]" ];
      cli = "--secret";
      property = "Secret";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    securityLabelDisable = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--security-opt label=disable";
      property = "SecurityLabelDisable";
    };

    securityLabelFileType = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "usr_t";
      cli = "--security-opt label=filetype:...";
      property = "SecurityLabelFileType";
    };

    securityLabelLevel = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "s0:c1,c2";
      cli = "--security-opt label=level:s0:c1,c2";
      property = "SecurityLabelLevel";
    };

    securityLabelNested = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--security-opt label=nested";
      property = "SecurityLabelNested";
    };

    securityLabelType = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "spc_t";
      cli = "--security-opt label=type:...";
      property = "SecurityLabelType";
    };

    shmSize = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "100m";
      cli = "--shm-size";
      property = "ShmSize";
    };

    startWithPod = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      description = "If pod is defined, container is started by pod";
      property = "StartWithPod";
    };

    stopSignal = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "SIGINT";
      cli = "--stop-signal";
      property = "StopSignal";
    };

    stopTimeout = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 20;
      cli = "--stop-timeout";
      property = "StopTimeout";
    };

    subGIDMap = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "gtest";
      cli = "--subgidname";
      property = "SubGIDMap";
    };

    subUIDMap = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "utest";
      cli = "--subuidname";
      property = "SubUIDMap";
    };

    sysctl = quadletOptions.mkOption {
      type = types.attrsOf types.str;
      default = { };
      example = {
        name = "value";
      };
      cli = "--sysctl";
      property = "Sysctl";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    timezone = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "local";
      cli = "--tz";
      property = "Timezone";
    };

    tmpfses = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/work" ];
      cli = "--tmpfs";
      property = "Tmpfs";
    };

    uidMaps = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "0:10000:10" ];
      cli = "--uidmap";
      property = "UIDMap";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    ulimits = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "nofile=1000:10000" ];
      cli = "--ulimit";
      property = "Ulimit";
    };

    unmask = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "ALL";
      cli = "--security-opt unmask=...";
      property = "Unmask";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    user = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "bin";
      cli = "--user";
      property = "User";
    };

    userns = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "keep-id:uid=200,gid=210";
      cli = "--userns";
      property = "UserNS";
    };

    volumes = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/source:/dest" ];
      cli = "--volume";
      property = "Volume";
    };

    workdir = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "$HOME";
      cli = "--workdir";
      property = "WorkingDir";
    };
  };

  serviceConfigDefault = {
    Restart = "always";
    TimeoutStartSec = 900;
  };
in
{
  options = quadletOptions.mkObjectOptions "container" {
    containerConfig = containerOpts;
  };

  config =
    let
      containerName = if config.containerConfig.name != null then config.containerConfig.name else name;
      containerConfig = config.containerConfig // {
        name = containerName;
      };
      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
      unitConfig = {
        Unit = {
          Description = "Podman container ${name}";
        }
        // config.unitConfig;
        Container = quadletUtils.configToProperties containerConfig containerOpts;
        Service = serviceConfigDefault // config.serviceConfig;
      }
      // (if quadlet == { } then { } else { Quadlet = quadlet; });
    in
    lib.pipe
      {
        _serviceName = name;
        _configText =
          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
        _autoStart = config.autoStart;
        _autoEscapeRequired = quadletUtils.autoEscapeRequired containerConfig containerOpts;
        ref = "${name}.container";

        # quadlet default is "split" which does not work rootless under system systemd.
        containerConfig.cgroupsMode = lib.mkIf config._rootless (lib.mkDefault "enabled");
      }
      [
        (quadletOptions.applyRootlessConfig config)
      ];
}


================================================
FILE: docs/README.md
================================================
# Docs

To generate the documentation, run:

```sh
nix build './docs#book'
```


================================================
FILE: docs/flake.nix
================================================
{
  description = "quadlet-nix docs";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    quadlet-nix.url = "path:..";
  };

  outputs =
    {
      nixpkgs,
      quadlet-nix,
      self,
      ...
    }:
    let
      allSystems = [
        "x86_64-linux"
        "aarch64-linux"
      ];
      perSystem = f: nixpkgs.lib.genAttrs allSystems f;
    in
    {
      packages = perSystem (
        system:
        let
          pkgs = import nixpkgs { inherit system; };
          lib = pkgs.lib;
          buildDocs =
            module:
            let
              moduleFn = import module;
              # filters out assertions, config, etc. that cause problems.
              filteredModuleFn = args: { inherit (moduleFn args) options; };
              eval = lib.evalModules {
                modules = [
                  { _module.args.pkgs = pkgs; }
                  (lib.mirrorFunctionArgs moduleFn filteredModuleFn)
                ];
              };
              options = lib.filterAttrs (name: _: name != "_module") eval.options;
            in
            pkgs.nixosOptionsDoc { inherit options; };

          pages = {
            nixosModules.quadlet = buildDocs quadlet-nix.nixosModules.quadlet;
            homeManagerModules.quadlet = buildDocs quadlet-nix.homeManagerModules.quadlet;
          };

        in
        {
          inherit pages;

          book = pkgs.stdenv.mkDerivation {
            pname = "quadlet-nix-docs-book";
            version = "0.1";
            src = self;

            nativeBuildInputs = [
              pkgs.mdbook
            ];

            dontConfigure = true;
            dontFixup = true;

            buildPhase = ''
              runHook preBuild
              cp ${quadlet-nix}/README.md src/introduction.md
              cp ${pages.nixosModules.quadlet.optionsCommonMark} src/nixos-options.md
              cp ${pages.homeManagerModules.quadlet.optionsCommonMark} src/home-manager-options.md
              mdbook build
              runHook postBuild
            '';

            installPhase = ''
              runHook preInstall
              mv book $out
              runHook postInstall
            '';
          };
        }
      );
    };
}


================================================
FILE: docs/src/SUMMARY.md
================================================
# Contents

- [Introduction](./introduction.md)
- [NixOS Options](./nixos-options.md)
- [Home Manager Options](./home-manager-options.md)


================================================
FILE: flake.nix
================================================
{
  description = "NixOS and home-manager module for Podman Quadlets";

  outputs =
    { self }:
    {
      nixosModules.quadlet = ./nixos-module.nix;
      homeManagerModules.quadlet = ./home-manager-module.nix;
    };
}


================================================
FILE: home-manager-module.nix
================================================
{
  config,
  osConfig ? { },
  lib,
  pkgs,
  ...
}:
let
  inherit (lib) mergeAttrsList mkIf getExe;

  cfg = config.virtualisation.quadlet;
  quadletUtils = import ./utils.nix {
    inherit pkgs lib;
    inherit (import (pkgs.path + "/nixos/lib/utils.nix") { inherit lib config pkgs; }) systemdUtils;
    podmanPackage = osConfig.virtualisation.podman.package or pkgs.podman;
    autoEscape = config.virtualisation.quadlet.autoEscape;
  };
  quadletOptions = import ./options.nix {
    supportRootless = false;
    inherit lib quadletUtils;
  };
  activationScript = lib.hm.dag.entryBefore [ "reloadSystemd" ] ''
    mkdir -p '${config.xdg.configHome}/quadlet-nix/'
    ln -sf "''${XDG_RUNTIME_DIR:-/run/user/$UID}/systemd/generator/" '${config.xdg.configHome}/quadlet-nix/out'
  '';
in
{
  options.virtualisation.quadlet = quadletOptions.mkTopLevelOptions { };
  config =
    let
      allObjects = quadletOptions.getAllObjects cfg;
      enable = cfg.enable == true || (cfg.enable == null && allObjects != [ ]);
    in
    mkIf enable {
      assertions = quadletOptions.mkAssertions [ ] cfg;
      warnings =
        (quadletUtils.assertionsToWarnings [
          {
            assertion = enable -> (osConfig.virtualisation.quadlet.enable or true == true);
            message = ''
              The `virtualisation.quadlet.enable` in **NixOS config** is not set to true.
              The NixOS module is required to set up Podman and explicit enablement will be required in the future.
            '';
          }
        ])
        ++ (quadletOptions.mkWarnings [ ] cfg);

      home.activation.quadletNix = mkIf (lib.length allObjects > 0) activationScript;

      xdg.configFile =
        let
          configPathLink =
            (pkgs.linkFarm "quadlet-out-path" [
              {
                name = "quadlet-nix";
                path = "${config.xdg.configHome}/quadlet-nix";
              }
            ])
            + "/quadlet-nix";
        in
        mergeAttrsList (
          map (p: {
            # Install the .container, .network, etc files
            "containers/systemd/${p.ref}" = {
              text = p._configText;
            };
            # Import quadlet-generated unit as a dropin override.
            "systemd/user/${p._serviceName}.service.d/override.conf" = {
              source = "${configPathLink}/out/${p._serviceName}.service";
            };
          }) allObjects
        )
        // {
          # `systemctl`, `sleep`, etc. not found
          "systemd/user/podman-user-wait-network-online.service.d/override.conf" = {
            text = quadletUtils.unitConfigToText {
              Service.ExecSearchPath = [ "/run/current-system/sw/bin/" ];
            };
          };
        };

      systemd.user.services =
        mergeAttrsList (
          map (p: {
            # Inject hash for the activation process to detect changes.
            # Must be in the main file as it's the only thing home-manager switch process looks at.
            # WantedBy must be set through `systemd.user.services` which generates .targets.wants symlinks.
            # sd-switch only starts new services with those symlinks.
            ${p._serviceName} = {
              Unit.X-QuadletNixConfigHash = builtins.hashString "sha256" p._configText;
              Install.WantedBy = if p._autoStart then [ "default.target" ] else [ ];
            };
          }) allObjects
        )
        // {
          # TODO: link from ${pkgs.podman}/share/systemd/user/podman-auto-update.service
          # when https://github.com/containers/podman/issues/24637 is fixed.
          podman-auto-update = mkIf cfg.autoUpdate.enable {
            Unit = {
              Description = "Podman auto-update service";
              Documentation = "man:podman-auto-update(1)";
            };
            Service = {
              Type = "oneshot";
              ExecStart = "${getExe quadletUtils.podmanPackage} auto-update";
              ExecStartPost = "${getExe quadletUtils.podmanPackage} image prune -f";
              TimeoutStartSec = "900s";
              TimeoutStopSec = "10s";
            };
          };
        };

      systemd.user.timers.podman-auto-update = mkIf cfg.autoUpdate.enable {
        Unit = {
          Description = "Podman auto-update timer";
          Documentation = "man:podman-auto-update(1)";
        };
        Timer = {
          OnCalendar = cfg.autoUpdate.calendar;
          Persistent = true;
        };
        Install.WantedBy = [ "timers.target" ];
      };
    };
}


================================================
FILE: image.nix
================================================
{ quadletUtils, quadletOptions }:
{
  config,
  name,
  lib,
  ...
}:
let
  inherit (lib) types;
  inherit (quadletUtils) encoders;

  imageOpts = {
    allTags = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--all-tags";
      property = "AllTags";
    };

    arch = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "aarch64";
      cli = "--arch";
      property = "Arch";
    };

    authFile = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/etc/registry/auth.json";
      cli = "--authfile";
      property = "AuthFile";
    };

    certDir = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/etc/registry/certs";
      cli = "--cert-dir";
      property = "CertDir";
    };

    modules = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/etc/nvd.conf" ];
      cli = "--module";
      property = "ContainersConfModule";
    };

    creds = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "myname:mypassword";
      cli = "--creds";
      property = "Creds";
    };

    decryptionKey = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "/etc/registry.key";
      cli = "--decryption-key";
      property = "DecryptionKey";
    };

    globalArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--log-level=debug" ];
      description = "Additional command line arguments to insert between `podman` and `pull`";
      property = "GlobalArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    image = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "docker.io/library/nginx:latest";
      description = "Image specification";
      property = "Image";
    };

    tag = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "localhost/imagename";
      description = "FQIN of the referenced Image. Only meaningful when source is a file or directory archive. Used when resolving .image references.";
      property = "ImageTag";
    };

    os = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "windows";
      cli = "--os";
      property = "OS";
    };

    podmanArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--add-host foobar" ];
      description = "Additional command line arguments to insert after `podman pull`";
      property = "PodmanArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    policy = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "always";
      cli = "--policy";
      property = "Policy";
    };

    retry = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 5;
      cli = "--retry";
      property = "Retry";
    };

    retryDelay = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "5s";
      cli = "--retry-delay";
      property = "RetryDelay";
    };

    tlsVerify = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--tls-verify";
      property = "TLSVerify";
    };

    variant = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "arm/v7";
      cli = "--variant";
      property = "Variant";
    };
  };

  serviceConfigDefault = {
    TimeoutStartSec = 900;
  };
in
{
  options = quadletOptions.mkObjectOptions "image" {
    imageConfig = imageOpts;
  };

  config =
    let
      imageConfig = config.imageConfig;
      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
      unitConfig = {
        Unit = {
          Description = "Podman image ${name}";
        }
        // config.unitConfig;
        Image = quadletUtils.configToProperties imageConfig imageOpts;
        Service = serviceConfigDefault // config.serviceConfig;
      }
      // (if quadlet == { } then { } else { Quadlet = quadlet; });
    in
    lib.pipe
      {
        _serviceName = "${name}-image";
        _configText =
          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
        _autoStart = config.autoStart;
        _autoEscapeRequired = quadletUtils.autoEscapeRequired imageConfig imageOpts;
        ref = "${name}.image";
      }
      [
        (quadletOptions.applyRootlessConfig config)
      ];
}


================================================
FILE: network.nix
================================================
{ quadletUtils, quadletOptions }:
{
  config,
  name,
  lib,
  ...
}:
let
  inherit (lib) types getExe;
  inherit (quadletUtils) encoders;

  networkOpts = {
    modules = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/etc/nvd.conf" ];
      cli = "--module";
      property = "ContainersConfModule";
    };

    disableDns = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--disable-dns";
      property = "DisableDNS";
    };

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

    driver = quadletOptions.mkOption {
      type = types.nullOr (
        types.enum [
          "bridge"
          "macvlan"
          "ipvlan"
        ]
      );
      default = null;
      example = "bridge";
      cli = "--driver";
      property = "Driver";
    };

    gateways = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "192.168.55.3" ];
      cli = "--gateway";
      property = "Gateway";
    };

    globalArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--log-level=debug" ];
      description = "Additional command line arguments to insert between `podman` and `network create`";
      property = "GlobalArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    interfaceName = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "enp1";
      cli = "--interface-name";
      property = "InterfaceName";
    };

    internal = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--internal";
      property = "Internal";
    };

    ipamDriver = quadletOptions.mkOption {
      type = types.nullOr (
        types.enum [
          "host-local"
          "dhcp"
          "none"
        ]
      );
      default = null;
      example = "dhcp";
      cli = "--ipam-driver";
      property = "IPAMDriver";
    };

    ipRanges = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "192.168.55.128/25" ];
      cli = "--ip-range";
      property = "IPRange";
    };

    ipv6 = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--ipv6";
      property = "IPv6";
    };

    labels = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--label";
      property = "Label";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    name = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "foo";
      description = "Network name as in `podman network create foo`";
      property = "NetworkName";
    };

    networkDeleteOnStop = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      description = "When set to true the network is deleted when the service is stopped";
      property = "NetworkDeleteOnStop";
    };

    options = quadletOptions.mkOption {
      # TODO: drop string support and remove warning.
      type = types.oneOf [
        types.str
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        isolate = "true";
      };
      cli = "--opt";
      property = "Options";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    podmanArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--dns=192.168.55.1" ];
      description = "Additional command line arguments to insert after `podman network create`";
      property = "PodmanArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    subnets = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "192.5.0.0/16" ];
      cli = "--subnet";
      property = "Subnet";
    };
  };
in
{
  options = quadletOptions.mkObjectOptions "network" {
    networkConfig = networkOpts;
  };

  config =
    let
      networkName = if config.networkConfig.name != null then config.networkConfig.name else name;
      networkConfig = config.networkConfig // {
        name = networkName;
      };
      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
      unitConfig = {
        Unit = {
          Description = "Podman network ${name}";
        }
        // config.unitConfig;
        Network = quadletUtils.configToProperties networkConfig networkOpts;
        Service = {
          # TODO: switches to NetworkDeleteOnStop once podman in stable nixpkgs supports it
          ExecStop = "${getExe quadletUtils.podmanPackage} network rm ${networkName}";
        }
        // config.serviceConfig;
      }
      // (if quadlet == { } then { } else { Quadlet = quadlet; });
    in
    lib.pipe
      {
        _serviceName = "${name}-network";
        _configText =
          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
        _autoStart = config.autoStart;
        _autoEscapeRequired = quadletUtils.autoEscapeRequired networkConfig networkOpts;
        ref = "${name}.network";
      }
      [
        (quadletOptions.applyRootlessConfig config)
      ];
}


================================================
FILE: nixos-module.nix
================================================
{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib) mergeAttrsList mkIf;

  cfg = config.virtualisation.quadlet;
  quadletUtils = import ./utils.nix {
    inherit pkgs lib;
    inherit (import (pkgs.path + "/nixos/lib/utils.nix") { inherit lib config pkgs; }) systemdUtils;
    podmanPackage = config.virtualisation.podman.package;
    autoEscape = config.virtualisation.quadlet.autoEscape;
  };
  quadletOptions = import ./options.nix {
    supportRootless = true;
    inherit lib quadletUtils;
  };
in
{
  options.virtualisation.quadlet = quadletOptions.mkTopLevelOptions { };

  config =
    let
      allObjects = quadletOptions.getAllObjects cfg;
      # TODO: switch to `cfg.enable == true || (cfg.enable == null && allObjects != [])`
      # when home-manager users set `enable` explicitly.
      enable = cfg.enable == true || cfg.enable == null;
    in
    mkIf enable {
      assertions = quadletOptions.mkAssertions [ ] cfg;
      warnings = quadletOptions.mkWarnings [ ] cfg;

      virtualisation.podman.enable = true;
      environment.etc = mergeAttrsList (
        map (p: {
          "containers/systemd/${p.ref}" = {
            text = p._configText;
            mode = "0600";
          };
        }) allObjects
      );
      # The symlinks are not necessary for the services to be honored by systemd,
      # but necessary for NixOS activation process to pick them up for updates.
      systemd.packages = [
        (pkgs.linkFarm "quadlet-service-symlinks" (
          map (p: {
            name = "etc/systemd/system/${p._serviceName}.service";
            path = "/run/systemd/generator/${p._serviceName}.service";
          }) allObjects
        ))
      ];
      # Inject X-RestartIfChanged=${hash} for NixOS to detect changes.
      systemd.services = mergeAttrsList (
        map (p: {
          ${p._serviceName} = {
            overrideStrategy = "asDropin";
            unitConfig.X-QuadletNixConfigHash = builtins.hashString "sha256" p._configText;
            # systemd recommends multi-user.target over default.target.
            # https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html#default.target
            wantedBy = if p._autoStart then [ "multi-user.target" ] else [ ];
          }
          // p._overrides;
        }) allObjects
      );

      systemd.timers.podman-auto-update = mkIf cfg.autoUpdate.enable {
        timerConfig.OnCalendar = [
          ""
          cfg.autoUpdate.calendar
        ];
        wantedBy = [ "timers.target" ];
        overrideStrategy = "asDropin";
      };
    };
}


================================================
FILE: options.nix
================================================
{
  lib,
  quadletUtils,
  supportRootless,
}:
let
  mkOption =
    {
      property,
      cli ? null,
      description ? null,
      encoders ? null,
      ...
    }@attrs:
    let
      descForDesc = if description == null then "" else description + "\n\n";
      descForCli = if cli == null then "" else "and command line argument `${cli}`";
    in
    (lib.mkOption (
      lib.filterAttrs (
        name: _:
        !(builtins.elem name [
          "property"
          "cli"
          "encoders"
        ])
      ) attrs
    ))
    // {
      inherit property;
      inherit encoders;
      description = "${descForDesc}Maps to quadlet option `${property}`${descForCli}.";
    };

  quadletOpts = {
    defaultDependencies = mkOption {
      type = lib.types.nullOr lib.types.bool;
      default = null;
      description = "Add Quadlet’s default network dependencies to the unit";
      property = "DefaultDependencies";
    };
  };

  mkCommonObjectOptions =
    objectType:
    {
      quadletConfig = quadletOpts;

      autoStart = lib.mkOption {
        type = lib.types.bool;
        default = true;
        description = "When enabled, this ${objectType} is automatically started on boot.";
      };

      unitConfig = lib.mkOption {
        type = lib.types.attrsOf quadletUtils.unitOption;
        default = { };
        description = "systemd unit config passed through to [Unit] section.";
      };

      serviceConfig = lib.mkOption {
        type = lib.types.attrsOf quadletUtils.unitOption;
        default = { };
        description = "systemd service config passed through to [Service] section.";
      };

      rawConfig = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
        description = ''
          Raw quadlet config text. Using this will cause all other options
          contributing to quadlet files to be ignored. autoStart is not affected.
        '';
      };

      _serviceName = lib.mkOption {
        internal = true;
        description = "Name of the systemd service unit, without the .service suffix.";
      };

      _configText = lib.mkOption {
        internal = true;
        description = "Generated quadlet config text";
      };

      _autoStart = lib.mkOption {
        internal = true;
        description = "Whether the service is automatically started on boot.";
      };

      _autoEscapeRequired = lib.mkOption {
        internal = true;
        description = ''
          Whether `autoEscape` needs to be switched on for correct encoding.
          This is false if already on.
        '';
      };

      _rootless = lib.mkOption {
        internal = true;
        default = false;
        description = ''
          Whether to run rootless under system systemd.
        '';
      };

      _overrides = lib.mkOption {
        internal = true;
        default = { };
        description = ''
          Overrides to apply on systemd.services.<name>. Not applicable to user systemd.
        '';
      };

      ref = lib.mkOption {
        readOnly = true;
        description = ''
          Reference to this ${objectType} from other quadlets.

          Quadlet resolves this to object (e.g. container) names and sets up appropriate systemd dependencies.

          This is recognized for most quadlet native options, but not by Podman command line.
          Using this inside `podmanArgs` will therefore unlikely to work.
        '';
      };
    }
    // (
      if !supportRootless then
        { }
      else
        {
          rootlessConfig = {
            uid = lib.mkOption {
              type = lib.types.nullOr lib.types.int;
              default = null;
              description = "User ID to run rootless podman as";
            };
          };
        }
    );

  commonTopLevelOptions =
    let
      submoduleArgs = {
        inherit quadletUtils;
        quadletOptions = self;
      };
    in
    {
      builds = lib.mkOption {
        type = lib.types.attrsOf (lib.types.submodule (import ./build.nix submoduleArgs));
        default = { };
        description = "Image builds";
      };
      containers = lib.mkOption {
        type = lib.types.attrsOf (lib.types.submodule (import ./container.nix submoduleArgs));
        default = { };
        description = "Containers";
      };
      images = lib.mkOption {
        type = lib.types.attrsOf (lib.types.submodule (import ./image.nix submoduleArgs));
        default = { };
        description = "Image pulls";
      };
      networks = lib.mkOption {
        type = lib.types.attrsOf (lib.types.submodule (import ./network.nix submoduleArgs));
        default = { };
        description = "Networks";
      };
      pods = lib.mkOption {
        type = lib.types.attrsOf (lib.types.submodule (import ./pod.nix submoduleArgs));
        default = { };
        description = "Pods";
      };
      volumes = lib.mkOption {
        type = lib.types.attrsOf (lib.types.submodule (import ./volume.nix submoduleArgs));
        default = { };
        description = "Volumes";
      };
      enable = lib.mkOption {
        type = lib.types.nullOr lib.types.bool;
        default = null;
        description = "Enables quadlet-nix";
      };
      autoEscape = lib.mkOption {
        type = lib.types.bool;
        default = true;
        description = ''
          Enables appropriate quoting / escaping.
        '';
      };
      autoUpdate = {
        enable = lib.mkOption {
          type = lib.types.bool;
          default = false;
          description = "Enables podman auto update.";
        };
        calendar = lib.mkOption {
          type = lib.types.str;
          default = "*-*-* 00:00:00";
          description = "Schedule for podman auto update. See `systemd.time(7)` for details.";
        };
      };
    };

  getAllObjects =
    config:
    builtins.concatLists (
      map lib.attrValues [
        config.builds
        config.containers
        config.images
        config.networks
        config.pods
        config.volumes
      ]
    );

  self = {
    inherit mkOption quadletOpts;

    mkObjectOptions =
      objectType: extraOptions:
      lib.attrsets.unionOfDisjoint (mkCommonObjectOptions objectType) extraOptions;

    mkTopLevelOptions = extraOptions: lib.attrsets.unionOfDisjoint commonTopLevelOptions extraOptions;

    inherit getAllObjects;

    applyRootlessConfig =
      prev: cfg:
      let
        isEnabled = supportRootless && prev.rootlessConfig.uid != null;
        ifEnabled = lib.mkIf isEnabled;
        userService = "user@${toString prev.rootlessConfig.uid}.service";
      in
      quadletUtils.unionOfDisjointRecursive cfg {
        _rootless = isEnabled;
        serviceConfig.User = ifEnabled prev.rootlessConfig.uid;
        unitConfig.Wants = ifEnabled (lib.mkAfter [ "linger-users.service" ]);
        unitConfig.Requires = ifEnabled (lib.mkAfter [ userService ]);
        unitConfig.After = ifEnabled (
          lib.mkAfter [
            "linger-users.service"
            userService
          ]
        );
      };

    mkAssertions =
      extraAssertions: config:
      let
        containerPodConflicts = lib.lists.intersectLists (lib.attrNames config.containers) (
          lib.attrNames config.pods
        );
        nullImageArchiveTags = lib.attrNames (
          lib.filterAttrs (
            _: image:
            lib.strings.hasPrefix "docker-archive:" image.imageConfig.image && image.imageConfig.tag == null
          ) config.images
        );
      in
      [
        {
          assertion = containerPodConflicts == [ ];
          message = ''
            The container/pod names should be unique!
            See: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#podname
            The following names are not unique: ${lib.concatStringsSep " " containerPodConflicts}
          '';
        }
        {
          assertion = !(builtins.any (p: p._autoEscapeRequired) (getAllObjects config));
          message = ''
            `virtualisation.quadlet.autoEscape = true` is required because this configuration contains characters that require quoting or escaping.

            If you have manual quoting or escaping in place, please undo those and enable `autoEscape`.
          '';
        }
        {
          assertion = nullImageArchiveTags == [ ];
          message = ''
            The following images using `docker-archive:` must have the fully qualified name (FQDN) specified as a tag: ${lib.concatStringsSep " " nullImageArchiveTags}
          '';
        }
      ]
      ++ extraAssertions;

    mkWarnings =
      extraWarnings: config:
      (quadletUtils.assertionsToWarnings [
        {
          # TODO: drop string support and remove.
          assertion =
            !(builtins.any (p: builtins.isString p.networkConfig.options) (
              builtins.attrValues config.networks
            ));
          message = "String value in `virtualisation.quadlet.networks.*.networkConfig.options` is deprecated. Make it a list or attrset instead.";
        }
      ])
      ++ extraWarnings;
  };
in
self


================================================
FILE: pod.nix
================================================
{ quadletUtils, quadletOptions }:
{
  config,
  name,
  lib,
  ...
}:
let
  inherit (lib) types;
  inherit (quadletUtils) encoders pkgs;

  podOpts = {
    name = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "name";
      cli = "--name";
      property = "PodName";
    };

    addHosts = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "hostname:192.168.10.11" ];
      cli = "--add-host";
      property = "AddHost";
    };

    modules = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/etc/nvd.conf" ];
      cli = "--module";
      property = "ContainersConfModule";
    };

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

    dnsOptions = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "ndots:1" ];
      cli = "--dns-option";
      property = "DNSOption";
    };

    dnsSearches = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "foo.com" ];
      cli = "--dns-search";
      property = "DNSSearch";
    };

    exitPolicy = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "stop";
      cli = "--exit-policy";
      property = "ExitPolicy";
    };

    gidMaps = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "0:10000:10" ];
      cli = "--gidmap";
      property = "GIDMap";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    globalArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--log-level=debug" ];
      description = "Additional command line arguments to insert between `podman` and `pod create`";
      property = "GlobalArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    hostname = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "new-host-name";
      cli = "--hostname";
      property = "HostName";
    };

    ip = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "192.5.0.1";
      cli = "--ip";
      property = "IP";
    };

    ip6 = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "2001:db8::1";
      cli = "--ip6";
      property = "IP6";
    };

    labels = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--label";
      property = "Label";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    networks = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "host" ];
      cli = "--network";
      property = "Network";
    };

    networkAliases = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "name" ];
      cli = "--network-alias";
      property = "NetworkAlias";
    };

    podmanArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--cpus=2" ];
      description = "Additional command line arguments to insert after `podman pod create`";
      property = "PodmanArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    publishPorts = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "50-59" ];
      cli = "--publish";
      property = "PublishPort";
    };

    # ServiceName not supported as custom service names can make quadlet-nix lost.

    shmSize = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "100m";
      cli = "--shm-size";
      property = "ShmSize";
    };

    stopTimeout = quadletOptions.mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 20;
      cli = "--time";
      property = "StopTimeout";
    };

    subGIDMap = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "gtest";
      cli = "--subgidname";
      property = "SubGIDMap";
    };

    subUIDMap = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "utest";
      cli = "--subuidname";
      property = "SubUIDMap";
    };

    uidMaps = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "0:10000:10" ];
      cli = "--uidmap";
      property = "UIDMap";
      encoders.scalar = encoders.scalar.quotedUnescaped;
    };

    userns = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "keep-id:uid=200,gid=210";
      cli = "--userns";
      property = "UserNS";
    };

    volumes = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/source:/dest" ];
      cli = "--volume";
      property = "Volume";
    };
  };
in
{
  options = quadletOptions.mkObjectOptions "pod" {
    podConfig = podOpts;
  };

  config =
    let
      serviceConfigDefault = {
        Restart = "always";
        TimeoutStartSec = 900;
      };
      podName = if config.podConfig.name != null then config.podConfig.name else name;
      podConfig = config.podConfig // {
        name = podName;
      };
      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
      unitConfig = {
        Unit = {
          Description = "Podman pod ${name}";
        }
        // config.unitConfig;
        Pod = quadletUtils.configToProperties podConfig podOpts;
        Service = serviceConfigDefault // config.serviceConfig;
      }
      // (if quadlet == { } then { } else { Quadlet = quadlet; });
      rootlessPidFilePath = "/run/user/${toString config.rootlessConfig.uid}/%N.pid";
    in
    lib.pipe
      {
        _serviceName = "${name}-pod";
        _configText =
          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
        _autoEscapeRequired = quadletUtils.autoEscapeRequired podConfig podOpts;
        _autoStart = config.autoStart;
        ref = "${name}.pod";

        # [rootless hack]
        # Quadlet manages pod infra container with PIDFile= at %t/%N.pid, which
        # rootless process has no access to.
        # Simply pointing PIDFile= at another location does not help as system
        # systemd does not like PIDFIle= owned by unprivileged user while the
        # process is out of the service.
        # We therefore make it a Type=simple and wait for the pid in service.
        podConfig.podmanArgs = lib.mkIf config._rootless (
          lib.mkAfter [ "--infra-conmon-pidfile=${rootlessPidFilePath}" ]
        );
        serviceConfig.Type = lib.mkIf config._rootless (lib.mkDefault "simple");
        serviceConfig.ExecStart = lib.mkIf config._rootless (
          lib.mkDefault "/bin/sh -c \"${quadletUtils.podmanPackage}/bin/podman pod start ${podName} && (read pid < ${rootlessPidFilePath}; exec ${pkgs.coreutils}/bin/tail -f --pid \${pid:?})\""
        );
        serviceConfig.ExecStopPost = lib.mkIf config._rootless (
          lib.mkAfter [
            "${pkgs.coreutils}/bin/rm -f ${rootlessPidFilePath}"
          ]
        );

        # some options conflict with stock quadlet and thus need force overriding
        _overrides =
          quadletUtils.unionOfDisjointRecursive
            (
              # Type= as a singular field will be overwritten by Quadlet
              if builtins.hasAttr "Type" config.serviceConfig then
                { serviceConfig.Type = config.serviceConfig.Type; }
              else
                { }
            )
            (
              # Type=simple does not support multiple ExecStart
              if builtins.hasAttr "ExecStart" config.serviceConfig then
                {
                  serviceConfig.ExecStart = [
                    ""
                    config.serviceConfig.ExecStart
                  ];
                }
              else
                { }
            );
      }
      [
        (quadletOptions.applyRootlessConfig config)
      ];
}


================================================
FILE: tests/README.md
================================================
# Tests

To run all tests:

```sh
nix flake check \
    --override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \
    --override-input home-manager 'github:nix-community/home-manager/master' \
    --override-input test-config "path:$(pwd)/tests/x86_64-linux" \
    ./tests
```

To run individual test (e.g. `basic-rootful`):

```sh
nix run \
  --override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \
  --override-input home-manager 'github:nix-community/home-manager/master' \
  --override-input test-config "path:$(pwd)/tests/x86_64-linux" \
  './tests#checks.x86_64-linux.basic-rootful.driver'
```


================================================
FILE: tests/aarch64-linux/flake.nix
================================================
{
  outputs = _: {
    system = "aarch64-linux";
  };
}


================================================
FILE: tests/basic.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, ... }:
    {
      virtualisation.quadlet = {
        containers.nginx = {
          containerConfig.image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
          containerConfig.publishPorts = [ "8080:80" ];
          serviceConfig.TimeoutStartSec = "60";
        }
        // extraConfig;
      };
    };
  testScript = ''
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)

    html = machine.succeed("curl http://127.0.0.1:8080")
    assert "nginx" in html.lower()
  '';
}


================================================
FILE: tests/build.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, config, ... }:
    {
      virtualisation.quadlet =
        let
          inherit (config.virtualisation.quadlet) builds;
        in
        {
          builds.hello = {
            buildConfig = {
              file = "${pkgs.writeText "Containerfile" ''
                FROM docker-archive:${pkgs.dockerTools.examples.bash}
                CMD bash -c 'echo "Success" > /output/result.txt'
              ''}";
            };
          }
          // extraConfig;

          containers.hello = {
            containerConfig = {
              image = builds.hello.ref;
              volumes = [ "/tmp:/output" ];
            };
            serviceConfig = {
              RemainAfterExit = true;
            };
          }
          // extraConfig;
        };
    };

  testScript = ''
    machine.wait_for_unit("hello.service", user=systemd_user, timeout=30)

    assert machine.succeed("cat /tmp/result.txt").strip() == 'Success'
  '';
}


================================================
FILE: tests/container.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, ... }:
    {
      virtualisation.quadlet = {
        containers.nginx = {
          containerConfig.image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
          containerConfig.publishPorts = [ "8080:80" ];
          serviceConfig.TimeoutStartSec = "60";
          serviceConfig.Restart = "on-failure";
        }
        // extraConfig;
      };
    };
  testScript = ''
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
    assert 'nginx' in machine.succeed("curl http://127.0.0.1:8080").lower()
    containers = get_containers()
    assert containers.keys() == {"nginx"}
    if podman_user is not None:
      assert not get_containers(user=None)

    machine.stop_job("nginx", user=systemd_user)
    machine.fail("curl http://127.0.0.1:8080")
    assert not get_containers()

    machine.start_job("nginx", user=systemd_user)
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
    assert 'nginx' in machine.succeed("curl http://127.0.0.1:8080").lower()
    containers = get_containers()
    assert containers.keys() == {"nginx"}

    run_as("podman stop nginx", user=podman_user)
    wait_for_unit_inactive("nginx.service", user=systemd_user, timeout=10)
  '';
}


================================================
FILE: tests/escaping.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, ... }:
    {
      virtualisation.quadlet = {
        containers.write1 = {
          containerConfig = {
            image = "docker-archive:${pkgs.dockerTools.examples.bash}";
            # quotedUnescaped
            addCapabilities = [ "SYS_NICE" ];
            entrypoint = "bash";
            # quotedEscaped
            environments = {
              FOO = "aaa bbb $ccc \"ddd\n\n ";
              bar = "\"aaa\"";
              ONLY_SPACES = "aaa bbb";
            };
            # raw
            exec = "-c 'echo -n \"$FOO\" > /tmp/foo.txt; echo -n \"$bar\" > /tmp/bar.txt; echo -n \"$ONLY_SPACES\" > /tmp/only_spaces.txt'";
            volumes = [
              "/tmp:/tmp"
            ];
          };
          serviceConfig = {
            RemainAfterExit = true;
          };
        }
        // extraConfig;
        containers.write2 = {
          containerConfig = {
            image = "docker-archive:${pkgs.dockerTools.examples.bash}";
            environments = {
              BAZ = "aaa";
            };
            entrypoint = "bash";
            # oneLine
            exec = [
              "-c"
              "echo $@ $0 $BAZ > /tmp/baz.txt"
              "bbb"
              "ccc"
            ];
            volumes = [
              "/tmp:/tmp"
            ];
          };
          serviceConfig = {
            RemainAfterExit = true;
          };
        }
        // extraConfig;
        containers.write3 =
          let
            scriptName = "aaa bbb \n $ccc";
            scriptDir = toString (pkgs.writeTextDir scriptName "echo 8439b333258ba90e > /tmp/write3.txt");
          in
          {
            containerConfig = {
              image = "docker-archive:${pkgs.dockerTools.examples.bash}";
              entrypoint = [
                "bash"
                "/test/${scriptName}"
              ];
              volumes = [
                "/tmp:/tmp"
                "${scriptDir}:/test/"
              ];
            };
            serviceConfig = {
              RemainAfterExit = true;
            };
          }
          // extraConfig;
      };
    };
  testScript = ''
    machine.wait_for_unit("write1.service", user=systemd_user, timeout=30)
    machine.wait_for_unit("write2.service", user=systemd_user, timeout=30)
    machine.wait_for_unit("write3.service", user=systemd_user, timeout=30)

    machine.wait_for_file("/tmp/foo.txt", timeout=10)
    assert machine.succeed("cat /tmp/foo.txt") == 'aaa bbb $ccc "ddd\n\n '

    machine.wait_for_file("/tmp/bar.txt", timeout=10)
    assert machine.succeed("cat /tmp/bar.txt") == '"aaa"'

    machine.wait_for_file("/tmp/baz.txt", timeout=10)
    assert machine.succeed("cat /tmp/baz.txt") == 'ccc bbb aaa\n'

    machine.wait_for_file("/tmp/only_spaces.txt", timeout=10)
    assert machine.succeed("cat /tmp/only_spaces.txt") == 'aaa bbb'

    machine.wait_for_file("/tmp/write3.txt", timeout=10)
    assert machine.succeed("cat /tmp/write3.txt") == '8439b333258ba90e\n'
  '';

}


================================================
FILE: tests/flake.nix
================================================
# this is a separate flake to so home-manager isn't made a compulsory input.

{
  description = "quadlet-nix tests";

  # inputs path to be set in --override-input
  inputs = {
    nixpkgs.url = "path:/dev/null";

    quadlet-nix.url = "path:..";

    home-manager.url = "path:/dev/null";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";

    test-config.url = "path:/dev/null";
  };

  outputs =
    {
      test-config,
      nixpkgs,
      home-manager,
      quadlet-nix,
      ...
    }:
    let
      system = test-config.system;
      makeTestScript =
        {
          podmanUser,
          systemdUser,
          testScript,
        }:
        { nodes, ... }:
        ''
          import json
          from typing import Any, Optional

          podman_user = ${podmanUser}
          systemd_user = ${systemdUser}

          def run_as(command: str, *, user: Optional[str]) -> str:
            if user is not None:
              command = f"sudo -u {user} -- {command}"
            return machine.succeed(command)

          def wait_for_unit_inactive(unit: str, *, user: Optional[str], timeout: int) -> None:
            def check_active(_last_try: bool) -> bool:
              state = machine.get_unit_property(unit, "ActiveState", user)

              if state == "inactive":
                return True
              if state in ("active", "deactivating"):
                return False
              assert False, f"{unit} reached state {state}"

            with machine.nested(
              f"waiting for unit {unit}"
              + (f" with user {user}" if user is not None else "")
              + " to be inactive"
            ):
              retry(check_active, timeout)

          def get_containers(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:
            containers = json.loads(run_as("podman ps --format=json", user=user))
            return {name: container for container in containers for name in container["Names"]}

          def get_networks(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:
            networks = json.loads(run_as("podman network ls --format=json", user=user))
            return {network["name"]: network for network in networks}

          def get_pods(*, user: Optional[str] = podman_user) -> dict[str, dict[str, Any]]:
            pods = json.loads(run_as("podman pod ls --format=json", user=user))
            return {pod["Name"]: pod for pod in pods}

          def switch_to_specialisation(specialisation: str) -> str:
            return machine.succeed(f"${nodes.machine.system.build.toplevel}/specialisation/{specialisation}/bin/switch-to-configuration test")

          machine.wait_for_unit("default.target", user=None)
          if systemd_user is not None:
            machine.wait_for_unit("default.target", user=systemd_user)

          ${testScript}
        '';

      makeTestCase = template: args: if builtins.isFunction template then template args else template;

      runRootfulTest =
        {
          name,
          template,
          pkgs,
        }:
        let
          testCase = makeTestCase template {
            extraConfig = { };
            isHomeManager = false;
            home = "/root";
          };
          testConfig = testCase.testConfig;
          testScript = testCase.testScript;
          specialisation = testCase.specialisation or (_: { });
        in
        {
          name = name + "-rootful";
          testScript = makeTestScript {
            systemdUser = "None";
            podmanUser = "None";
            inherit testScript;
          };

          node.specialArgs.testType = "rootful";
          nodes.machine =
            { pkgs, ... }@attrs:
            {
              imports = [
                quadlet-nix.nixosModules.quadlet
                testConfig
              ];
              environment.systemPackages = [ pkgs.curl ];
              specialisation = builtins.mapAttrs (name: value: { configuration = value; }) (specialisation attrs);
            };
        };

      runRootlessTest =
        {
          name,
          template,
          pkgs,
        }:
        let
          testCase = makeTestCase template {
            extraConfig = {
              rootlessConfig.uid = 1357;
            };
            isHomeManager = false;
            home = "/home/alice";
          };
          testConfig = testCase.testConfig;
          testScript = testCase.testScript;
          specialisation = testCase.specialisation or (_: { });
        in
        {
          name = name + "-rootless";
          testScript = makeTestScript {
            systemdUser = "None";
            podmanUser = "\"alice\"";
            inherit testScript;
          };

          node.specialArgs.testType = "rootless";
          nodes.machine =
            { pkgs, ... }@attrs:
            {
              imports = [
                quadlet-nix.nixosModules.quadlet
                testConfig
              ];
              environment.systemPackages = [ pkgs.curl ];
              specialisation = builtins.mapAttrs (name: value: { configuration = value; }) (specialisation attrs);

              users.users.alice = {
                uid = 1357;
                group = "alice";
                linger = true;
                autoSubUidGidRange = true;
                isNormalUser = true;
              };
              users.groups.alice = {
                gid = 2468;
              };
            };
        };

      runHomeManagerTest =
        {
          name,
          template,
          pkgs,
        }:
        let
          testCase = makeTestCase template {
            extraConfig = { };
            isHomeManager = true;
            home = "/home/alice";
          };
          testConfig = testCase.testConfig;
          testScript = testCase.testScript;
          specialisation = testCase.specialisation or (_: { });
        in
        {
          name = name + "-home-manager";
          testScript = makeTestScript {
            systemdUser = "\"alice\"";
            podmanUser = "\"alice\"";
            inherit testScript;
          };

          nodes.machine =
            { lib, pkgs, ... }@attrs:
            {
              imports = [
                quadlet-nix.nixosModules.quadlet
                home-manager.nixosModules.home-manager
              ];
              virtualisation.quadlet.enable = true;
              environment.systemPackages = [ pkgs.curl ];

              # brings up network-online.target
              systemd.targets.test-network = {
                wants = [ "network-online.target" ];
                wantedBy = [ "multi-user.target" ];
              };

              users.users.alice = {
                group = "alice";
                linger = true;
                autoSubUidGidRange = true;
                isNormalUser = true;
              };
              users.groups.alice = { };

              home-manager.extraSpecialArgs.testType = "home-manager";
              home-manager.users.alice = lib.mkDefault (
                { config, ... }:
                {
                  imports = [
                    quadlet-nix.homeManagerModules.quadlet
                    testConfig
                  ];
                  home.stateVersion = config.home.version.release;
                }
              );

              specialisation = builtins.mapAttrs (name: value: {
                configuration = {
                  home-manager.users.alice = (
                    { config, ... }:
                    {
                      imports = [
                        quadlet-nix.homeManagerModules.quadlet
                        testConfig
                        value
                      ];
                      home.stateVersion = config.home.version.release;
                    }
                  );
                };
              }) (specialisation attrs);
            };
        };

      genTest =
        pkgs: runTest: template:
        let
          name = pkgs.lib.removeSuffix ".nix" (builtins.baseNameOf template);
          test = pkgs.testers.runNixOSTest (runTest {
            template = import template;
            inherit name pkgs;
          });
        in
        {
          name = test.config.name;
          value = test;
        };

    in
    {
      checks =
        let
          pkgs = import nixpkgs { inherit system; };
          lib = pkgs.lib;
          tests = builtins.listToAttrs (
            map ({ runner, template }: genTest pkgs runner template) (
              lib.cartesianProduct {
                template = [
                  ./basic.nix
                  ./build.nix
                  ./container.nix
                  ./image.nix
                  ./network.nix
                  ./pod.nix
                  ./volume.nix
                  ./switch.nix
                  ./raw.nix
                  ./health.nix
                  ./escaping.nix
                  ./overriding.nix
                ];
                runner = [
                  runRootfulTest
                  runRootlessTest
                  runHomeManagerTest
                ];
              }
            )
          );
        in
        {
          "${system}" = tests;
        };
    };
}


================================================
FILE: tests/health.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, ... }:
    {
      virtualisation.quadlet = {
        containers.good = {
          containerConfig = {
            image = "docker-archive:${pkgs.dockerTools.examples.redis}";
            healthCmd = "redis-cli ping || exit 1";
            healthRetries = 1;
          };
          serviceConfig.TimeoutStartSec = 60;
        }
        // extraConfig;
        containers.bad = {
          containerConfig = {
            image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
            healthCmd = "exit 1";
            healthRetries = 1;
          };
          serviceConfig.TimeoutStartSec = 60;
        }
        // extraConfig;
      };
    };

  testScript = ''
    machine.wait_for_unit("good.service", user=systemd_user, timeout=30)
    machine.wait_for_unit("bad.service", user=systemd_user, timeout=30)
    machine.sleep(2)  # wait for health command cycles

    containers = get_containers()
    assert containers.keys() == {"good", "bad"}
    assert "(healthy)" in containers["good"]["Status"]
    assert "(unhealthy)" in containers["bad"]["Status"]
  '';
}


================================================
FILE: tests/image.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, config, ... }:
    {
      virtualisation.quadlet =
        let
          inherit (config.virtualisation.quadlet) images;
        in
        {
          images.hello =
            let
              test-bash-image = pkgs.dockerTools.buildImage {
                name = "whatever.com/test-bash";
                tag = "latest";
                fromImage = pkgs.dockerTools.examples.bash;
              };
            in
            {
              imageConfig = {
                image = "docker-archive:${test-bash-image}";
                tag = "whatever.com/test-bash:latest";
              };
            }
            // extraConfig;

          containers.hello = {
            containerConfig = {
              image = images.hello.ref;
              volumes = [ "/tmp:/output" ];
              entrypoint = "bash";
              exec = [
                "-c"
                "echo \"Success\" > /output/result.txt"
              ];
            };
            serviceConfig = {
              RemainAfterExit = true;
            };
          }
          // extraConfig;
        };
    };

  testScript = ''
    machine.wait_for_unit("hello.service", user=systemd_user, timeout=30)

    assert machine.succeed("cat /tmp/result.txt").strip() == 'Success'
  '';
}


================================================
FILE: tests/network.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, config, ... }:
    {
      virtualisation.quadlet =
        let
          inherit (config.virtualisation.quadlet) networks;
        in
        {
          containers.nginx = {
            containerConfig = {
              image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
              publishPorts = [ "8080:80" ];
              networks = [
                networks.foo.ref
                networks.bar.ref
              ];
            };
          }
          // extraConfig;
          networks.foo = {
            networkConfig.options.isolate = "true";
          }
          // extraConfig;
          networks.bar = { } // extraConfig;
        };
    };
  testScript = ''
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
    assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()

    containers = get_containers()
    assert containers.keys() == {"nginx"}
    networks = get_networks()
    assert networks.keys() == {"foo", "bar", "podman"}
    assert set(containers["nginx"]["Networks"]) == {"foo", "bar"}
    assert networks["foo"]["options"]["isolate"] == "true"
    if podman_user is not None:
      assert not get_containers(user=None)
      assert get_networks(user=None).keys() == {"podman"}

    machine.stop_job("foo-network", user=systemd_user)
    machine.fail("curl http://127.0.0.1:8080")
    assert not get_containers()
    networks = get_networks()
    assert networks.keys() == {"bar", "podman"}

    machine.start_job("nginx", user=systemd_user)
    assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
    containers = get_containers()
    assert containers.keys() == {"nginx"}
    networks = get_networks()
    assert networks.keys() == {"foo", "bar", "podman"}
  '';
}


================================================
FILE: tests/overriding.nix
================================================
{ extraConfig, isHomeManager, ... }:
{
  testConfig =
    { pkgs, lib, ... }:
    let
      execStartPre = "${pkgs.bash}/bin/bash -c 'echo ef1e835e0ae5 > /tmp/foo.txt'";
      nixosOverrides = {
        systemd.services.nginx.serviceConfig.ExecStartPre = execStartPre;
      };
      homeManagerOverrides = {
        systemd.user.services.nginx.Service.ExecStartPre = execStartPre;
      };
      overrides = if isHomeManager then homeManagerOverrides else nixosOverrides;
    in
    {
      virtualisation.quadlet = {
        containers.nginx = {
          containerConfig.image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
          containerConfig.publishPorts = [ "8080:80" ];
          serviceConfig.TimeoutStartSec = "60";
        }
        // extraConfig;
      };
    }
    // overrides;
  testScript = ''
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)

    html = machine.succeed("curl http://127.0.0.1:8080")
    assert "nginx" in html.lower()
    assert machine.succeed("cat /tmp/foo.txt").strip() == "ef1e835e0ae5"
  '';
}


================================================
FILE: tests/pod.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, config, ... }:
    {
      virtualisation.quadlet =
        let
          inherit (config.virtualisation.quadlet) pods;
        in
        {
          containers.nginx = {
            containerConfig = {
              image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
              pod = pods.foo.ref;
            };
            serviceConfig.Restart = "on-failure";
          }
          // extraConfig;
          containers.redis = {
            containerConfig = {
              image = "docker-archive:${pkgs.dockerTools.examples.redis}";
              pod = pods.foo.ref;
            };
            serviceConfig.Restart = "on-failure";
          }
          // extraConfig;
          pods.foo = {
            podConfig = {
              publishPorts = [ "8080:80" ];
            };
          }
          // extraConfig;
        };
    };
  testScript = ''
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
    machine.wait_for_unit("redis.service", user=systemd_user, timeout=30)
    assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()

    containers = get_containers()
    assert len(containers) == 3
    assert containers.keys() >= {"nginx", "redis"}
    pods = get_pods()
    assert pods.keys() == {"foo"}
    assert set(c["Id"] for c in pods["foo"]["Containers"]) == {c["Id"] for c in containers.values()}
    if podman_user is not None:
      assert not get_containers(user=None)
      assert not get_pods(user=None)

    machine.stop_job("foo-pod", user=systemd_user)
    machine.fail("curl http://127.0.0.1:8080")
    assert not get_containers()
    assert not get_pods()

    machine.start_job("nginx", user=systemd_user)
    assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
    machine.wait_for_unit("foo-pod.service", user=systemd_user, timeout=30)
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)
    machine.wait_for_unit("redis.service", user=systemd_user, timeout=30)
    containers = get_containers()
    assert len(containers) == 3
    assert containers.keys() >= {"nginx", "redis"}
    pods = get_pods()
    assert pods.keys() == {"foo"}

    run_as("podman pod stop foo", user=podman_user)
    wait_for_unit_inactive("foo-pod.service", user=systemd_user, timeout=10)
    wait_for_unit_inactive("nginx.service", user=systemd_user, timeout=10)
    wait_for_unit_inactive("redis.service", user=systemd_user, timeout=10)
  '';
}


================================================
FILE: tests/raw.nix
================================================
{ extraConfig, ... }:
{
  testConfig =
    { pkgs, ... }:
    {
      virtualisation.quadlet = {
        containers.nginx = {
          rawConfig = ''
            [Container]
            Image=docker-archive:${pkgs.dockerTools.examples.nginx}
            PublishPort=8080:80
            [Service]
            TimeoutStartSec=60
          '';
        }
        // extraConfig;
      };
    };
  testScript = ''
    machine.wait_for_unit("nginx.service", user=systemd_user, timeout=30)

    html = machine.succeed("curl http://127.0.0.1:8080")
    assert "nginx" in html.lower()
  '';
}


================================================
FILE: tests/switch.nix
================================================
{ extraConfig, ... }:
let
  makeQuadletConfig = pkgs: networks: {
    containers.nginx = {
      containerConfig = {
        image = "docker-archive:${pkgs.dockerTools.examples.nginx}";
        publishPorts = [ "8080:80" ];
        networks = map (x: "${x}.network") networks;
      };
    }
    // extraConfig;
    networks = builtins.listToAttrs (
      map (x: {
        name = x;
        value = {
          networkConfig.name = x;
        }
        // extraConfig;
      }) networks
    );
  };
in
{
  testConfig =
    { lib, pkgs, ... }:
    {
      virtualisation.quadlet = lib.mkDefault (makeQuadletConfig pkgs [ "foo" ]);
    };

  specialisation =
    { pkgs, ... }:
    {
      step1Add.virtualisation.quadlet = makeQuadletConfig pkgs [
        "foo"
        "bar"
      ];
      step2Remove.virtualisation.quadlet = makeQuadletConfig pkgs [ "bar" ];
      step3AddRemove.virtualisation.quadlet = makeQuadletConfig pkgs [ "baz" ];
    };

  testScript = ''
    def check(expected_networks: set[str]) -> None:
      assert "nginx" in machine.succeed("curl http://127.0.0.1:8080").lower()
      containers = get_containers()
      assert containers.keys() == {"nginx"}
      networks = get_networks()
      assert networks.keys() == expected_networks | {"podman"}
      assert set(containers["nginx"]["Networks"]) == expected_networks

    check({"foo"})

    switch_to_specialisation("step1Add")
    check({"foo", "bar"})

    switch_to_specialisation("step2Remove")
    check({"bar"})

    switch_to_specialisation("step3AddRemove")
    check({"baz"})
  '';
}


================================================
FILE: tests/volume.nix
================================================
{ extraConfig, home, ... }:
{
  testConfig =
    { pkgs, config, ... }:
    {
      virtualisation.quadlet =
        let
          inherit (config.virtualisation.quadlet) volumes;
        in
        {
          containers.write = {
            containerConfig = {
              image = "docker-archive:${pkgs.dockerTools.examples.bash}";
              entrypoint = "bash";
              exec = "-c 'echo 262c837a9160 > /mnt/foo/bar.txt'";
              volumes = [
                "${volumes.foo.ref}:/mnt/foo"
              ];
            };
            serviceConfig = {
              RemainAfterExit = true;
            };
          }
          // extraConfig;
          volumes.foo = {
            volumeConfig = {
              type = "bind";
              device = home;
            };
          }
          // extraConfig;
        };
    };
  testScript = ''
    machine.wait_for_unit("write.service", user=systemd_user, timeout=30)

    path = "${home}/bar.txt"
    machine.wait_for_file(path, timeout=10)
    assert machine.succeed(f"cat {path}").strip() == "262c837a9160"
  '';
}


================================================
FILE: tests/x86_64-linux/flake.nix
================================================
{
  outputs = _: {
    system = "x86_64-linux";
  };
}


================================================
FILE: utils.nix
================================================
{
  pkgs,
  lib,
  systemdUtils,
  podmanPackage,
  autoEscape,
}:

let
  # encodes value based on how podman parses them
  # see: https://github.com/containers/podman/blob/main/pkg/systemd/quadlet/quadlet.go
  encoders =
    let
      # wraps a scalar encoder so it tries not escaping if possible
      makePassive =
        f: x:
        let
          raw = systemdUtils.lib.toOption x;
          encoded = f x;
          canSkip = encoded == raw || (builtins.match ".*[ \t\n\r].*" raw == null && "\"${raw}\"" == encoded);
        in
        if canSkip then raw else encoded;

    in
    {
      scalar.legacy = systemdUtils.lib.toOption;

      # Lookup, LookupAll, LookupLast, LookupAllRaw, LookupLastRaw
      scalar.raw =
        x:
        let
          ret = systemdUtils.lib.toOption x;
        in
        if builtins.match ".*[\r\n].*" ret == null then
          ret
        else
          throw "quadlet-nix internal error: unsafe value for scalar.raw option: ${ret}";

      # LookupAllArgs, LookupAllKeyVal
      # same as systemdUtils.lib.serviceToUnit
      scalar.quotedEscaped = makePassive builtins.toJSON;

      # LookupAllStrv
      scalar.quotedUnescaped = makePassive (
        x:
        let
          escaped = builtins.toJSON x;
          unescaped = "\"${systemdUtils.lib.toOption x}\"";
        in
        if escaped == unescaped then
          unescaped
        else
          throw "quadlet-nix internal error: unsafe value for scalar.quotedUnescaped option: ${escaped}"
      );

      list.default = fScalar: x: map fScalar x;

      # LookupLastArgs
      list.oneLine = fScalar: x: builtins.concatStringsSep " " (map fScalar x);

      list.json = builtins.toJSON;

      attrs.default = fScalar: x: lib.mapAttrsToList (k: v: "${k}=${fScalar v}") x;
    };

  encode =
    encoders: value:
    if builtins.isString value || builtins.isInt value || builtins.isBool value then
      encoders.scalar value
    else if builtins.isList value then
      encoders.list value
    else if builtins.isAttrs value then
      encoders.attrs value
    else
      throw "quadlet-nix internal error: unexpected type for encoder";

  finalizeEncoders =
    autoEscape: optionEncoders:
    let
      effEncoders = if autoEscape then optionEncoders else { scalar = encoders.scalar.legacy; };
      scalar = effEncoders.scalar or encoders.scalar.raw;
      list = effEncoders.list or (encoders.list.default scalar);
      attrs = effEncoders.attrs or (encoders.attrs.default scalar);
    in
    {
      inherit scalar list attrs;
    };

  configToProperties =
    autoEscape: config: options:
    let
      nonNullConfig = lib.filterAttrs (_: value: value != null) config;
      encodeEntry =
        name: value:
        lib.nameValuePair options.${name}.property (
          encode (finalizeEncoders autoEscape options.${name}.encoders) value
        );
    in
    lib.mapAttrs' encodeEntry nonNullConfig;

  unionOfDisjointRecursive =
    x: y:
    let
      intersectionY = builtins.intersectAttrs x y;
      intersectionX = builtins.mapAttrs (n: _: x.${n}) intersectionY;
      mergeFn =
        name: values:
        let
          x = builtins.elemAt values 0;
          y = builtins.elemAt values 1;
        in
        if builtins.isAttrs x && builtins.isAttrs y then
          unionOfDisjointRecursive x y
        else if x == y then
          x
        else
          throw "unionOfDisjointRecursive: collision on ${name}";
      merged = builtins.zipAttrsWith mergeFn [
        intersectionX
        intersectionY
      ];
    in
    x // y // merged;

in
{
  configToProperties = config: options: configToProperties autoEscape config options;
  autoEscapeRequired =
    config: options:
    configToProperties autoEscape config options != configToProperties true config options;

  unitConfigToText =
    unitConfig:
    builtins.concatStringsSep "\n\n" (
      lib.mapAttrsToList (
        name: section: "[${name}]\n${systemdUtils.lib.attrsToSection section}"
      ) unitConfig
    );

  assertionsToWarnings =
    asssertions: map (x: x.message) (builtins.filter (x: !x.assertion) asssertions);

  inherit (systemdUtils.unitOptions) unitOption;
  inherit
    pkgs
    podmanPackage
    encoders
    unionOfDisjointRecursive
    ;
}


================================================
FILE: volume.nix
================================================
{ quadletUtils, quadletOptions }:
{
  config,
  name,
  lib,
  ...
}:
let
  inherit (lib) types;
  inherit (quadletUtils) encoders;

  volumeOpts = {
    name = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "foo";
      description = "Volume name as in `podman volume create foo`";
      property = "VolumeName";
    };

    copy = quadletOptions.mkOption {
      type = types.nullOr types.bool;
      default = null;
      cli = "--opt copy";
      property = "Copy";
    };

    device = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "tmpfs";
      cli = "--opt device=...";
      property = "Device";
    };

    driver = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "image";
      cli = "--driver";
      property = "Driver";
    };

    globalArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--log-level=debug" ];
      description = "Additional command line arguments to insert between `podman` and `volume create`";
      property = "GlobalArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    group = quadletOptions.mkOption {
      type = types.nullOr (
        types.oneOf [
          types.int
          types.str
        ]
      );
      default = null;
      example = 192;
      cli = "--opt group=...";
      property = "Group";
    };

    image = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "quay.io/centos/centos:latest";
      cli = "--opt image=...";
      property = "Image";
    };

    labels = quadletOptions.mkOption {
      type = types.oneOf [
        (types.listOf types.str)
        (types.attrsOf types.str)
      ];
      default = { };
      example = {
        foo = "bar";
      };
      cli = "--label";
      property = "Label";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    modules = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "/etc/nvd.conf" ];
      cli = "--module";
      property = "ContainersConfModule";
    };

    options = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      cli = "--opt o=...";
      property = "Options";
    };

    podmanArgs = quadletOptions.mkOption {
      type = types.listOf types.str;
      default = [ ];
      example = [ "--driver=image" ];
      description = "Additional command line arguments to insert after `podman volume create`";
      property = "PodmanArgs";
      encoders.scalar = encoders.scalar.quotedEscaped;
    };

    type = quadletOptions.mkOption {
      type = types.nullOr types.str;
      default = null;
      cli = "--opt type=...";
      description = "Filesystem type of `device`";
      property = "Type";
    };

    user = quadletOptions.mkOption {
      type = types.nullOr (
        types.oneOf [
          types.int
          types.str
        ]
      );
      default = null;
      example = 123;
      cli = "--opt uid=...";
      property = "User";
    };
  };
in
{
  options = quadletOptions.mkObjectOptions "volume" {
    volumeConfig = volumeOpts;
  };

  config =
    let
      volumeName = if config.volumeConfig.name != null then config.volumeConfig.name else name;
      volumeConfig = config.volumeConfig // {
        name = volumeName;
      };
      quadlet = quadletUtils.configToProperties config.quadletConfig quadletOptions.quadletOpts;
      unitConfig = {
        Unit = {
          Description = "Podman volume ${name}";
        }
        // config.unitConfig;
        Volume = quadletUtils.configToProperties volumeConfig volumeOpts;
        Service = config.serviceConfig;
      }
      // (if quadlet == { } then { } else { Quadlet = quadlet; });
    in
    lib.pipe
      {
        _serviceName = "${name}-volume";
        _configText =
          if config.rawConfig != null then config.rawConfig else quadletUtils.unitConfigToText unitConfig;
        _autoStart = config.autoStart;
        _autoEscapeRequired = quadletUtils.autoEscapeRequired volumeConfig volumeOpts;
        ref = "${name}.volume";
      }
      [
        (quadletOptions.applyRootlessConfig config)
      ];
}
Download .txt
gitextract_vh7gx8rf/

├── .github/
│   └── workflows/
│       └── test.yml
├── LICENSE
├── README.md
├── build.nix
├── container.nix
├── docs/
│   ├── README.md
│   ├── flake.nix
│   └── src/
│       └── SUMMARY.md
├── flake.nix
├── home-manager-module.nix
├── image.nix
├── network.nix
├── nixos-module.nix
├── options.nix
├── pod.nix
├── tests/
│   ├── README.md
│   ├── aarch64-linux/
│   │   └── flake.nix
│   ├── basic.nix
│   ├── build.nix
│   ├── container.nix
│   ├── escaping.nix
│   ├── flake.nix
│   ├── health.nix
│   ├── image.nix
│   ├── network.nix
│   ├── overriding.nix
│   ├── pod.nix
│   ├── raw.nix
│   ├── switch.nix
│   ├── volume.nix
│   └── x86_64-linux/
│       └── flake.nix
├── utils.nix
└── volume.nix
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (128K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 2549,
    "preview": "name: test\n\non:\n  push:\n  pull_request:\n  schedule:\n  - cron: '0 16 * * *'  # UTC 16:00 daily\n\njobs:\n  format:\n    runs-"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2023 SEIAROTg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 14377,
    "preview": "# quadlet-nix\n\nManages Podman containers, networks, pods, etc. on NixOS via [Quadlet](https://docs.podman.io/en/latest/m"
  },
  {
    "path": "build.nix",
    "chars": 7371,
    "preview": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils"
  },
  {
    "path": "container.nix",
    "chars": 21271,
    "preview": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils"
  },
  {
    "path": "docs/README.md",
    "chars": 79,
    "preview": "# Docs\n\nTo generate the documentation, run:\n\n```sh\nnix build './docs#book'\n```\n"
  },
  {
    "path": "docs/flake.nix",
    "chars": 2242,
    "preview": "{\n  description = \"quadlet-nix docs\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n    quadl"
  },
  {
    "path": "docs/src/SUMMARY.md",
    "chars": 138,
    "preview": "# Contents\n\n- [Introduction](./introduction.md)\n- [NixOS Options](./nixos-options.md)\n- [Home Manager Options](./home-ma"
  },
  {
    "path": "flake.nix",
    "chars": 224,
    "preview": "{\n  description = \"NixOS and home-manager module for Podman Quadlets\";\n\n  outputs =\n    { self }:\n    {\n      nixosModul"
  },
  {
    "path": "home-manager-module.nix",
    "chars": 4535,
    "preview": "{\n  config,\n  osConfig ? { },\n  lib,\n  pkgs,\n  ...\n}:\nlet\n  inherit (lib) mergeAttrsList mkIf getExe;\n\n  cfg = config.vi"
  },
  {
    "path": "image.nix",
    "chars": 4865,
    "preview": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils"
  },
  {
    "path": "network.nix",
    "chars": 5627,
    "preview": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types getExe;\n  inherit (quadl"
  },
  {
    "path": "nixos-module.nix",
    "chars": 2573,
    "preview": "{\n  config,\n  lib,\n  pkgs,\n  ...\n}:\nlet\n  inherit (lib) mergeAttrsList mkIf;\n\n  cfg = config.virtualisation.quadlet;\n  q"
  },
  {
    "path": "options.nix",
    "chars": 9103,
    "preview": "{\n  lib,\n  quadletUtils,\n  supportRootless,\n}:\nlet\n  mkOption =\n    {\n      property,\n      cli ? null,\n      descriptio"
  },
  {
    "path": "pod.nix",
    "chars": 8574,
    "preview": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils"
  },
  {
    "path": "tests/README.md",
    "chars": 616,
    "preview": "# Tests\n\nTo run all tests:\n\n```sh\nnix flake check \\\n    --override-input nixpkgs 'github:NixOS/nixpkgs/nixos-unstable' \\"
  },
  {
    "path": "tests/aarch64-linux/flake.nix",
    "chars": 56,
    "preview": "{\n  outputs = _: {\n    system = \"aarch64-linux\";\n  };\n}\n"
  },
  {
    "path": "tests/basic.nix",
    "chars": 560,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.ngin"
  },
  {
    "path": "tests/build.nix",
    "chars": 991,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n     "
  },
  {
    "path": "tests/container.nix",
    "chars": 1274,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.ngin"
  },
  {
    "path": "tests/escaping.nix",
    "chars": 3035,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.writ"
  },
  {
    "path": "tests/flake.nix",
    "chars": 9261,
    "preview": "# this is a separate flake to so home-manager isn't made a compulsory input.\n\n{\n  description = \"quadlet-nix tests\";\n\n  "
  },
  {
    "path": "tests/health.nix",
    "chars": 1129,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.good"
  },
  {
    "path": "tests/image.nix",
    "chars": 1316,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n     "
  },
  {
    "path": "tests/network.nix",
    "chars": 1818,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n     "
  },
  {
    "path": "tests/overriding.nix",
    "chars": 1072,
    "preview": "{ extraConfig, isHomeManager, ... }:\n{\n  testConfig =\n    { pkgs, lib, ... }:\n    let\n      execStartPre = \"${pkgs.bash}"
  },
  {
    "path": "tests/pod.nix",
    "chars": 2504,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let\n     "
  },
  {
    "path": "tests/raw.nix",
    "chars": 585,
    "preview": "{ extraConfig, ... }:\n{\n  testConfig =\n    { pkgs, ... }:\n    {\n      virtualisation.quadlet = {\n        containers.ngin"
  },
  {
    "path": "tests/switch.nix",
    "chars": 1571,
    "preview": "{ extraConfig, ... }:\nlet\n  makeQuadletConfig = pkgs: networks: {\n    containers.nginx = {\n      containerConfig = {\n   "
  },
  {
    "path": "tests/volume.nix",
    "chars": 1090,
    "preview": "{ extraConfig, home, ... }:\n{\n  testConfig =\n    { pkgs, config, ... }:\n    {\n      virtualisation.quadlet =\n        let"
  },
  {
    "path": "tests/x86_64-linux/flake.nix",
    "chars": 55,
    "preview": "{\n  outputs = _: {\n    system = \"x86_64-linux\";\n  };\n}\n"
  },
  {
    "path": "utils.nix",
    "chars": 4263,
    "preview": "{\n  pkgs,\n  lib,\n  systemdUtils,\n  podmanPackage,\n  autoEscape,\n}:\n\nlet\n  # encodes value based on how podman parses the"
  },
  {
    "path": "volume.nix",
    "chars": 4337,
    "preview": "{ quadletUtils, quadletOptions }:\n{\n  config,\n  name,\n  lib,\n  ...\n}:\nlet\n  inherit (lib) types;\n  inherit (quadletUtils"
  }
]

About this extraction

This page contains the full source code of the SEIAROTg/quadlet-nix GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (117.3 KB), approximately 29.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!