podman: fix container config mount on Darwin

On Darwin, `services.podman` mounts `~/.config/containers` into the Fedora CoreOS VM, but this did not work correctly for two reasons:

* `xdg.configFile` creates symlinks into `/nix/store`, which are broken inside the guest.
* The mount target `~/\.config/containers` is not canonical on Fedora CoreOS, so Podman rejects it.

To fix this, we now:

* materialize the generated Podman config files as real files with `runCommand`
* sync them into `~/.config/containers` during activation, between `linkGeneration` and `podmanMachines`
* use the canonical guest path `/var/home/<user>/.config/containers`

Because adding the config directory to the volume mounts overrides the defaults, we also restore the default Podman volumes as the defaults for the  `machines.<machine>.volumes` attribute while still allowing full overrides.

This change does not affect Linux: `xdg.configFile` still produces store symlinks there.

Closes #9327.
This commit is contained in:
Thierry Delafontaine
2026-05-21 15:44:24 +02:00
committed by Austin Horstman
parent c53d0150e9
commit 3d64f2875e
5 changed files with 132 additions and 64 deletions

View File

@@ -83,15 +83,16 @@ let
volumes = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
default = [
"/Users:/Users"
"/private:/private"
"/var/folders:/var/folders"
];
example = [
"/Users:/Users"
];
description = ''
Volumes to mount in the machine, specified as source:target pairs.
If empty, podman will use its default volume mounts.
'';
};
@@ -183,7 +184,6 @@ in
timezone = "UTC";
volumes = [
"/Users:/Users"
"/private:/private"
];
autoStart = true;
watchdogInterval = 30;
@@ -231,7 +231,11 @@ in
swap = null;
timezone = null;
username = null;
volumes = [ ];
volumes = [
"/Users:/Users"
"/private:/private"
"/var/folders:/var/folders"
];
autoStart = true;
watchdogInterval = 30;
};
@@ -249,15 +253,18 @@ in
];
}
(mkIf pkgs.stdenv.isDarwin {
(mkIf pkgs.stdenv.hostPlatform.isDarwin {
home.activation.podmanMachines =
let
mkMachineInitScript =
name: machine:
let
# Automatically mount host's container config into the VM
# Automatically mount host's container config into the VM.
# The guest target uses /var/home/<user> rather than /home/<user>
# because on Fedora CoreOS /home is a filesystem-wide symlink
# to /var/home, and podman requires a canonical mount path.
username = if isNull machine.username then "core" else machine.username;
configVolume = "$HOME/.config/containers:/home/${username}/.config/containers";
configVolume = "$HOME/.config/containers:/var/home/${username}/.config/containers";
allVolumes = [ configVolume ] ++ machine.volumes;
in
''

View File

@@ -7,6 +7,22 @@
let
cfg = config.services.podman;
toml = pkgs.formats.toml { };
configFiles = {
"policy.json" =
if cfg.settings.policy != { } then
pkgs.writeText "policy.json" (builtins.toJSON cfg.settings.policy)
else
"${pkgs.skopeo.policy}/default-policy.json";
"registries.conf" = toml.generate "registries.conf" {
registries = lib.mapAttrs (_n: v: { registries = v; }) cfg.settings.registries;
};
"storage.conf" = toml.generate "storage.conf" cfg.settings.storage;
"containers.conf" = toml.generate "containers.conf" cfg.settings.containers;
}
// lib.optionalAttrs (cfg.settings.mounts != [ ]) {
"mounts.conf" = pkgs.writeText "mounts.conf" (builtins.concatStringsSep "\n" cfg.settings.mounts);
};
in
{
meta.maintainers = [
@@ -25,6 +41,20 @@ in
package = lib.mkPackageOption pkgs "podman" { };
_configFiles = lib.mkOption {
type = lib.types.attrsOf lib.types.path;
internal = true;
visible = false;
readOnly = true;
default = configFiles;
description = ''
Attribute set mapping `~/.config/containers/<name>` to the generated
source path. Consumed by the Linux module to populate `xdg.configFile`
and by the Darwin module to install the same files as real files into
`~/.config/containers` for the podman machine bind mount.
'';
};
settings = {
containers = lib.mkOption {
inherit (toml) type;
@@ -94,24 +124,28 @@ in
services.podman.settings.storage.storage.driver = lib.mkDefault "overlay";
# Configuration files are written to `$XDG_CONFIG_HOME/containers`
# On Linux: podman reads them directly from this location
# On Darwin: these files are automatically mounted into the podman machine VM
# (see darwin.nix for the volume mount configuration)
xdg.configFile = {
"containers/policy.json".source =
if cfg.settings.policy != { } then
pkgs.writeText "policy.json" (builtins.toJSON cfg.settings.policy)
else
"${pkgs.skopeo.policy}/default-policy.json";
"containers/registries.conf".source = toml.generate "registries.conf" {
registries = lib.mapAttrs (_n: v: { registries = v; }) cfg.settings.registries;
};
"containers/storage.conf".source = toml.generate "storage.conf" cfg.settings.storage;
"containers/containers.conf".source = toml.generate "containers.conf" cfg.settings.containers;
"containers/mounts.conf" = lib.mkIf (cfg.settings.mounts != [ ]) {
text = builtins.concatStringsSep "\n" cfg.settings.mounts;
};
};
# On Linux, podman reads its configuration directly from
# `$XDG_CONFIG_HOME/containers`. On Darwin the same files are placed there
# as real files by an activation script and bind-mounted into the podman
# machine VM (see darwin.nix).
xdg.configFile = lib.mkIf pkgs.stdenv.hostPlatform.isLinux (
lib.mapAttrs' (name: src: lib.nameValuePair "containers/${name}" { source = src; }) cfg._configFiles
);
home.activation.podmanContainersConfig = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin (
lib.hm.dag.entryBetween [ "podmanMachines" ] [ "linkGeneration" ] ''
run mkdir -p "$HOME/.config/containers"
# Remove only files this module manages.
for f in ${lib.escapeShellArgs (builtins.attrNames cfg._configFiles)}; do
run rm -f "$HOME/.config/containers/$f"
done
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: src: ''run install -m 0644 ${src} "$HOME/.config/containers/${name}"''
) cfg._configFiles
)}
''
);
};
}

View File

@@ -39,42 +39,50 @@
};
};
nmt.script = ''
configPath=home-files/.config/containers
containersFile=$configPath/containers.conf
policyFile=$configPath/policy.json
registriesFile=$configPath/registries.conf
storageFile=$configPath/storage.conf
mountsFile=$configPath/mounts.conf
nmt.script =
if pkgs.stdenv.hostPlatform.isDarwin then
''
# On Darwin, container config files are not part of the home-files
# generation they're installed into ~/.config/containers by an
# activation script so they can be bind-mounted into the podman VM.
assertFileExists activate
assertFileRegex activate 'podmanContainersConfig'
assertFileRegex activate 'install -m 0644'
assertFileRegex activate 'policy\.json'
assertFileRegex activate 'registries\.conf'
assertFileRegex activate 'storage\.conf'
assertFileRegex activate 'containers\.conf'
assertFileRegex activate 'mounts\.conf'
# Check that config files are generated on both platforms
assertFileExists $containersFile
assertFileExists $policyFile
assertFileExists $registriesFile
assertFileExists $storageFile
assertFileExists $mountsFile
# Verify that config directory is automatically mounted into podman
# machines at the canonical /var/home path
assertFileRegex activate '\$HOME/\.config/containers:/var/home/core/\.config/containers'
''
else
''
configPath=home-files/.config/containers
containersFile=$configPath/containers.conf
policyFile=$configPath/policy.json
registriesFile=$configPath/registries.conf
storageFile=$configPath/storage.conf
mountsFile=$configPath/mounts.conf
containersFile=$(normalizeStorePaths $containersFile)
policyFile=$(normalizeStorePaths $policyFile)
registriesFile=$(normalizeStorePaths $registriesFile)
storageFile=$(normalizeStorePaths $storageFile)
mountsFile=$(normalizeStorePaths $mountsFile)
assertFileExists $containersFile
assertFileExists $policyFile
assertFileExists $registriesFile
assertFileExists $storageFile
assertFileExists $mountsFile
assertFileContent $containersFile ${./configuration-containers-expected.conf}
assertFileContent $policyFile ${./configuration-policy-expected.json}
assertFileContent $registriesFile ${./configuration-registries-expected.conf}
assertFileContent $storageFile ${./configuration-storage-expected.conf}
assertFileContent $mountsFile ${./configuration-mounts-expected.conf}
containersFile=$(normalizeStorePaths $containersFile)
policyFile=$(normalizeStorePaths $policyFile)
registriesFile=$(normalizeStorePaths $registriesFile)
storageFile=$(normalizeStorePaths $storageFile)
mountsFile=$(normalizeStorePaths $mountsFile)
${
if pkgs.stdenv.hostPlatform.isDarwin then
''
# Darwin-specific: verify that config directory is automatically mounted into podman machines
assertFileExists activate
assertFileRegex activate '\$HOME/\.config/containers:/home/core/\.config/containers'
''
else
""
}
'';
assertFileContent $containersFile ${./configuration-containers-expected.conf}
assertFileContent $policyFile ${./configuration-policy-expected.json}
assertFileContent $registriesFile ${./configuration-registries-expected.conf}
assertFileContent $storageFile ${./configuration-storage-expected.conf}
assertFileContent $mountsFile ${./configuration-mounts-expected.conf}
'';
}

View File

@@ -27,9 +27,21 @@
assertFileNotRegex activate '[-][-]swap'
assertFileNotRegex activate '[-][-]timezone'
assertFileNotRegex activate '[-][-]username'
assertFileNotRegex activate '[-][-]volumes'
assertFileRegex activate '[-][-]volume "$HOME/.config/containers:/var/home/core/.config/containers"'
assertFileRegex activate '[-][-]volume "/Users:/Users"'
assertFileRegex activate '[-][-]volume "/private:/private"'
assertFileRegex activate '[-][-]volume "/var/folders:/var/folders"'
# Verify that config directory is automatically mounted into the machine
assertFileRegex activate '\$HOME/\.config/containers:/home/core/\.config/containers'
# at the canonical /var/home path (because /home is a symlink on the guest)
assertFileRegex activate '\$HOME/\.config/containers:/var/home/core/\.config/containers'
# Verify the install-based config materialization is wired in
assertFileRegex activate 'podmanContainersConfig'
assertFileRegex activate 'install -m 0644'
assertFileRegex activate 'policy\.json'
assertFileRegex activate 'registries\.conf'
assertFileRegex activate 'storage\.conf'
assertFileRegex activate 'containers\.conf'
'';
}

View File

@@ -16,6 +16,7 @@
memory = 4096;
diskSize = 50;
autoStart = false;
volumes = [ ];
watchdogInterval = 30;
};
};
@@ -46,6 +47,10 @@
assertFileRegex activate '[-][-]memory 8192'
assertFileRegex activate '[-][-]disk-size 200'
assertFileRegex activate '[-][-]rootful'
assertFileRegex activate '[-][-]volume "$HOME/.config/containers:/var/home/core/.config/containers"'
assertFileRegex activate '[-][-]volume "/Users:/Users"'
assertFileRegex activate '[-][-]volume "/private:/private"'
assertFileRegex activate '[-][-]volume "/var/folders:/var/folders"'
# Check test-machine initialization
assertFileRegex activate 'test-machine'
@@ -53,11 +58,13 @@
assertFileRegex activate '[-][-]cpus 2'
assertFileRegex activate '[-][-]memory 4096'
assertFileRegex activate '[-][-]disk-size 50'
assertFileRegex activate '[-][-]volume "$HOME/.config/containers:/var/home/core/.config/containers"'
# Verify default machine is NOT created
assertFileNotRegex activate 'podman-machine-default'
# Verify that config directory is automatically mounted into all machines
assertFileRegex activate '\$HOME/\.config/containers:/home/core/\.config/containers'
# at the canonical /var/home path (because /home is a symlink on the guest)
assertFileRegex activate '\$HOME/\.config/containers:/var/home/core/\.config/containers'
'';
}