podman: add module

This module is a continuation of #2630 by MaeIsBad.

It also adds a module `virtualisation.oci-containers` that is
equivalent to the one in NixOS. Basically it allows a simple toggle to
activate oci-container services and commands.

We also support Podman on mac. Note, Podman requires a VM on mac,
which has to be started before any Podman commands can be executed.
Users might sometimes require VMs that use different architectures
than the default VM started by Podman. Thus, they get the option to
define the VM(s) that will be initialized and started by podman.

Since Podman has to start a machine, it's best to do it using launchd.
The configuration of the machines requires a JSON, generated from an
attrset in Home Manager, which is where Python script comes into play
to take care of diff-ing the `podman machine list` to CRUD them.

PR #4331

Co-authored-by: MaeIsBad <26093674+MaeIsBad@users.noreply.github.com>
This commit is contained in:
Michael Vogel 2023-08-11 10:42:57 +02:00 committed by Robert Helgesson
parent 07c322a7cf
commit faa4b16358
No known key found for this signature in database
GPG key ID: 36BDAA14C2797E89
16 changed files with 1128 additions and 0 deletions

View file

@ -1348,6 +1348,16 @@ in
A new module is available: 'programs.gradle'.
'';
}
{
time = "2023-12-23T08:45:52+00:00";
message = ''
Three new modules are available:
'virtualisation.containers',
'virtualisation.oci-containers',
'virtualisation.podman'.
'';
}
];
};
}

View file

@ -368,6 +368,9 @@ let
./systemd.nix
./targets/darwin
./targets/generic-linux.nix
./virtualisation/containers.nix
./virtualisation/oci-containers.nix
./virtualisation/podman/podman.nix
./xresources.nix
./xsession.nix
./misc/nix.nix

View file

@ -0,0 +1,76 @@
{ config, lib, pkgs, ... }:
let
cfg = config.virtualisation.containers;
inherit (lib) mkOption types;
toml = pkgs.formats.toml { };
in {
meta.maintainers = [ lib.maintainers.michaelCTS ];
options.virtualisation.containers = {
enable = lib.mkEnableOption "the common containers configuration module";
ociSeccompBpfHook.enable = lib.mkEnableOption "the OCI seccomp BPF hook";
registries = {
search = mkOption {
type = types.listOf types.str;
default = [ "docker.io" "quay.io" ];
description = ''
List of repositories to search.
'';
};
insecure = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
List of insecure repositories.
'';
};
block = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
List of blocked repositories.
'';
};
};
policy = mkOption {
type = types.attrs;
default = { };
example = lib.literalExpression ''
{
default = [ { type = "insecureAcceptAnything"; } ];
transports = {
docker-daemon = {
"" = [ { type = "insecureAcceptAnything"; } ];
};
};
}
'';
description = ''
Signature verification policy file.
If this option is empty the default policy file from
`skopeo` will be used.
'';
};
};
config = lib.mkIf cfg.enable {
xdg.configFile."containers/registries.conf".source =
toml.generate "registries.conf" {
registries = lib.mapAttrs (n: v: { registries = v; }) cfg.registries;
};
xdg.configFile."containers/policy.json".source = if cfg.policy != { } then
pkgs.writeText "policy.json" (builtins.toJSON cfg.policy)
else
"${pkgs.skopeo.src}/default-policy.json";
};
}

View file

@ -0,0 +1,28 @@
# Equivalent of
# https://github.com/NixOS/nixpkgs/blob/nixos-unstable/nixos/modules/virtualisation/oci-containers.nix
{ config, lib, pkgs, ... }:
let
cfg = config.virtualisation.oci-containers;
inherit (lib) mkDefault mkIf mkMerge mkOption types;
defaultBackend = "podman";
in {
meta.maintainers = [ pkgs.lib.maintainers.michaelCTS ];
options.virtualisation.oci-containers = {
enable = lib.mkEnableOption
"a convenience option to enable containers in platform-agnostic manner";
backend = mkOption {
type = types.enum [ "podman" ];
default = defaultBackend;
description = "Which service to use as a backend for containers.";
};
};
config = mkIf (cfg.enable && cfg.backend == "podman") {
virtualisation.podman.enable = true;
};
}

View file

@ -0,0 +1,30 @@
# podmactl
`podmactl` is a script to manage the podman machines declared in Home
Manager.
## How it works
`main()` is a (hopefully) straight-forward method to read, but the gist of it is:
1. The declared machines and their configuration are passed in.
2. Existing machines and their configuration are listed.
3. A diff is made from the declared machines and existing machines.
4. New machines are added.
5. Existing machines are updated.
6. Old machines are removed.
7. The machine declared as `active` is started (if necessary).
## Developing
Enter a devshell with `nix-shell`.
Make your changes and then run
```
# Code autoformatting
black .
# Unittests
python -m unittest
```

View file

@ -0,0 +1,28 @@
{ pkgs ? (import <nixpkgs> { }), }:
pkgs.stdenv.mkDerivation {
name = "podmactl";
src = ./.;
buildInputs = [ pkgs.python311 ];
doCheck = true;
checkPhase = ''
runHook preCheck
(
cd $src
black --check .
python -m unittest
)
runHook postCheck
'';
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp podmactl.py $out/bin/podmactl
chmod +x $out/bin/podmactl
runHook postInstall
'';
}

View file

@ -0,0 +1,317 @@
#!/usr/bin/env python3.11
import argparse
import json
import logging
import re
import shlex
import subprocess
import sys
from argparse import ArgumentParser
from dataclasses import asdict, dataclass, field, fields
from functools import reduce
from operator import concat
from typing import Dict, Generic, Iterable, List, Optional, TypeVar
DEFAULT_MACHINE = "podman-machine-default"
T = TypeVar("T")
logger = logging.getLogger("podman-launchd")
logger_commander = logger.getChild("commander")
CAMEL_REGEX = re.compile(r"([A-Z]+)")
UNDERSCORE_REGEX = re.compile(r"^_")
@dataclass(frozen=True)
class Machine:
# Resource config for CLI
cpus: int
disk_size: int
memory: int
# Metadata about the machine
name: str
active: bool = field(compare=False, default=False)
qemu_binary: Optional[str] = None
"""A path to a custom QEMU command to be used when starting the machine with a specific arch"""
# Optional CLI parameters
image_path: Optional[str] = None
"""A local path to a custom QEMU image"""
@classmethod
def from_dict(cls, a_dict: dict) -> "Machine":
return Machine(
**{
snake_key: value
for key, value in a_dict.items()
if (snake_key := camel2snake(key).lower()) in MACHINE_FIELDS
}
)
MACHINE_FIELDS = [field.name for field in fields(Machine)]
@dataclass
class Diff(Generic[T]):
new: List[T] = field(default_factory=list)
modified: List[T] = field(default_factory=list)
same: List[T] = field(default_factory=list)
removed: List[T] = field(default_factory=list)
class PodmanMachineCommander:
MACHINE_CLI_ARGS = ("cpus", "disk_size", "memory")
def __init__(self, command: str = None):
self.command = command or "podman"
def _call(self, *args: str, **kwargs) -> str:
"""Call podman machine"""
args_ = [self.command, "machine"] + list(args)
logger_commander.debug("Executing %s", shlex.join(args_))
# no subprocess.run here as streaming is necessary
stdout_lines = []
with subprocess.Popen(
args_,
# Capture both streams in stdout
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
**kwargs,
) as process:
# Collect stdout+stderr and steam if requested
for line in process.stdout:
line_str = line.decode().rstrip()
stdout_lines.append(line_str)
logger_commander.debug(line_str)
stdout = "\n".join(stdout_lines)
# Check if the command failed
if (return_code := process.returncode) != 0:
print(stdout, file=sys.stderr)
raise subprocess.CalledProcessError(return_code, args_, stdout)
return stdout
def _call_json(self, *args: str, **kwargs) -> dict:
"""Call podman requesting JSON output and interpret it as such"""
return json.loads(self._call(*args, "--format", "json", **kwargs))
@classmethod
def make_cli_args(
cls, machine: Machine, selected_args: Iterable[str] = MACHINE_CLI_ARGS
):
"""
Converts dict from list of key-value pair
to list of ["--key1", value1, "--key2", value2, ... ]
"""
machine_dict = asdict(machine)
return reduce(
concat,
[
[("--" + key.replace("_", "-")), str(value)]
for key in selected_args
if (value := machine_dict.get(key))
],
[],
)
def get_active_machine_name(self) -> str:
"""Name of the machine that is currently running"""
output_json = self._call_json("info")
return output_json.get("Host", {}).get("CurrentMachine")
def list(self) -> Dict[str, Machine]:
"""Get all machines known to podman"""
machine_jsons = self._call_json("list")
if not isinstance(machine_jsons, list):
raise ValueError("Unexpected output from command", machine_jsons)
# `podman machine list` has different units for disk_size, memory, etc.
# `podman machine inspect` has the information we need
inspected_jsons = self.inspect(
*[listed_machine["Name"] for listed_machine in machine_jsons]
)
return {
machine.name: machine
for inspected_json in inspected_jsons
if (
machine := Machine.from_dict(
{"name": inspected_json["Name"], **inspected_json["Resources"]}
)
)
}
def inspect(self, *machine_names: str):
"""Get information about a machine from podman"""
# The podman machine interface is really confusing
# inspect only returns JSON and other commands require --format json
return json.loads(self._call("inspect", *machine_names))
def add(self, machine: Machine):
"""
Let podman create a machine's config and initialize it
Also downloads the image of the machine
"""
self._call(
"init",
*self.make_cli_args(machine, self.MACHINE_CLI_ARGS + ("image_path",)),
machine.name,
)
def update(self, machine: Machine):
"""Update a machine's configuration and write it to disk"""
self._call("set", *self.make_cli_args(machine), machine.name)
# Set the custom QEMU path in the machine's config
# This is necessary for running machines with a specific architecture
if machine.qemu_binary:
inspection = self.inspect(machine.name)[0]
config_path = inspection.get("ConfigPath", {}).get("Path", {})
with open(config_path) as config_file:
config = json.load(config_file)
if not (cmd_line := config.get("CmdLine")):
logger.error(
"Cannot find CmdLine in config of %s at", machine.name, config_path
)
cmd_line[0] = machine.qemu_binary
with open(config_path, mode="w") as config_file:
json.dump(config, config_file)
def remove(self, machine: Machine):
"""Kills and removes the machine"""
self._call("rm", "--force", machine.name)
def start(self, machine_name: str):
"""Start up a machine"""
self._call("start", machine_name)
def stop(self, machine_name: str):
"""Stop a running machine"""
self._call("stop", machine_name)
def main(
requested_machines: Dict[str, Machine],
podman_command: str,
):
"""
:param requested_machines: Which machines should exist on the host
:param podman_command: The path to or the podman command itself
"""
podman_command = podman_command or "podman"
commander = PodmanMachineCommander(podman_command)
active_machines = [
name for name, machine in requested_machines.items() if machine.active
]
if len(active_machines) != 1:
raise ValueError("Exactly one machine in the configuration should be active")
requested_active = active_machines[0]
old_machines = commander.list()
# Find machines to add, update, delete
diffs = diff_machines(requested_machines, old_machines)
# Init new machines
for new_machine in diffs.new:
logger.info("Adding machine: %s. This may take some time...", new_machine.name)
commander.add(new_machine)
# Init the default machine if it's not
# Delete old ones
for removed_machine in diffs.removed:
logger.info("Removing machine: %s", removed_machine.name)
commander.remove(removed_machine)
# Update configuration of qemuBinary if necessary
for mod_machine in diffs.modified:
logger.info("Updating machine: %s", mod_machine.name)
commander.update(mod_machine)
# Start the requested machine if it isn't already running
active_machine = commander.get_active_machine_name()
if active_machine != requested_active:
if active_machine:
logger.info("Stopping machine: %s", active_machine)
commander.stop(active_machine)
logger.info("Starting: %s", requested_active)
commander.start(requested_active)
logger.info("%s is active and podman is ready to be used")
def camel2snake(camel: str) -> str:
"""
Converts camelCase to snake_case
"""
snake = CAMEL_REGEX.sub(r"_\1", camel).lower()
# if snake starts with _ remove it
return UNDERSCORE_REGEX.sub("", snake)
def diff_machines(
requested_machines: Dict[str, Machine], old_machines: Dict[str, Machine]
) -> Diff[Machine]:
diff: Diff[Machine] = Diff()
requested_names = requested_machines.keys()
old_names = old_machines.keys()
requested_items = requested_machines.items()
old_items = old_machines.items()
diff.new = [requested_machines[name] for name in (requested_names - old_names)]
diff.removed = [old_machines[name] for name in (old_names - requested_names)]
diff.same = list(dict(old_items & requested_items).values())
# Find modified machines = same key, different Machine
diff.modified = [
requested_machines[key]
for key in (requested_names & old_names)
if requested_machines[key] != old_machines[key]
]
return diff
def MachineDict(json_path: str) -> dict:
try:
with open(json_path) as json_file:
loaded_json = json.load(json_file)
return {
name: Machine.from_dict({"name": name, **machine})
for name, machine in loaded_json.items()
}
except json.JSONDecodeError as decode_error:
raise argparse.ArgumentTypeError() from decode_error
except Exception as exc:
raise argparse.ArgumentTypeError() from exc
if __name__ == "__main__":
parser = ArgumentParser(
"podman-launchd", description="CRUDs pod machines and starts one"
)
parser.add_argument(
"machines",
help="Path to JSON configuration of machines that should be on this host",
type=MachineDict,
)
parser.add_argument(
"-p", "--podman", help="Name or path of the podman command to use"
)
parser.add_argument(
"--verbose", help="Activate verbose logging", action="store_true"
)
cmd_args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if cmd_args.verbose else logging.INFO)
try:
main(cmd_args.machines, cmd_args.podman)
except:
logger.exception("Couldn't complete command")
exit(1)

View file

@ -0,0 +1,5 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
buildInputs = [ (pkgs.python311.withPackages (ps: with ps; [ black ])) ];
}

View file

@ -0,0 +1,232 @@
import argparse
import json
import tempfile
import unittest
from podmactl import (
Machine,
MachineDict,
diff_machines,
PodmanMachineCommander,
)
class MachineTestCase(unittest.TestCase):
def test_from_list_dict(self):
"""Ensure that dicts from `podman machine list` can create a machine object"""
self.assertEqual(
Machine(
cpus=2, disk_size=100, memory=2048, name="indie-machine", active=True
),
Machine.from_dict(
dict(
CPUs=2,
DiskSize=100,
Memory=2048,
Name="indie-machine",
)
),
)
def test_from_extra_dict(self):
self.assertEqual(
Machine(
cpus=2, disk_size=100, memory=2048, name="indie-machine", active=True
),
Machine.from_dict(
dict(
cpus=2,
disk_size=100,
memory=2048,
name="indie-machine",
active=True,
new=True,
dont_exist="something",
something_else="ladidah",
)
),
)
def test_from_bad_dict(self):
"""Will pass the wrong number of args to the __init__"""
self.assertRaises(
TypeError,
Machine.from_dict,
dict(
cpus=2,
),
)
def test_make_cli_args(self):
args = PodmanMachineCommander.make_cli_args(
Machine(cpus=2, disk_size=50, memory=4096, name="manjaro", active=False)
)
self.assertEqual(
args,
[
"--cpus",
"2",
"--disk-size",
"50",
"--memory",
"4096",
],
)
def test_make_optional_cli_args(self):
machine = Machine(
cpus=2,
disk_size=50,
memory=4096,
name="manjaro",
active=False,
image_path="somewhere.qcow2.xz",
)
self.assertEqual(
PodmanMachineCommander.make_cli_args(machine),
[
"--cpus",
"2",
"--disk-size",
"50",
"--memory",
"4096",
],
)
self.assertEqual(
PodmanMachineCommander.make_cli_args(
machine, PodmanMachineCommander.MACHINE_CLI_ARGS + ("image_path",)
),
[
"--cpus",
"2",
"--disk-size",
"50",
"--memory",
"4096",
"--image-path",
"somewhere.qcow2.xz",
],
)
class MachineDictTestCase(unittest.TestCase):
def test_load(self):
machine_json = {
"cpus": 2,
"disk_size": 100,
"memory": 2048,
"active": False,
}
machines_json = {"default": machine_json}
with tempfile.NamedTemporaryFile(mode="w") as fp:
json.dump(machines_json, fp)
fp.seek(0)
machines = MachineDict(fp.name)
self.assertDictEqual(
machines,
{
"default": Machine(
cpus=2,
disk_size=100,
memory=2048,
name="default",
active=False,
)
},
)
def test_bad_machine_load(self):
with self.assertRaises(argparse.ArgumentTypeError):
with tempfile.NamedTemporaryFile(mode="w") as fp:
json.dump({"default": {}}, fp)
fp.seek(0)
MachineDict(fp.name)
def test_bad_json_load(self):
with self.assertRaises(argparse.ArgumentTypeError):
with tempfile.NamedTemporaryFile(mode="w") as fp:
fp.write("this is definitely not a json")
fp.seek(0)
MachineDict(fp.name)
class DiffTestCase(unittest.TestCase):
def test_new_machines(self):
diff = diff_machines(
{
"new": Machine(
cpus=1, disk_size=100, memory=1024, name="new", active=True
),
"old": Machine(
cpus=1, disk_size=100, memory=1024, name="old", active=False
),
},
{
"old": Machine(
cpus=1, disk_size=100, memory=1024, name="old", active=False
),
},
)
self.assertListEqual(
diff.new,
[Machine(cpus=1, disk_size=100, memory=1024, name="new", active=False)],
)
self.assertListEqual(
diff.same,
[Machine(cpus=1, disk_size=100, memory=1024, name="old", active=False)],
)
self.assertListEqual(diff.removed, [])
self.assertListEqual(diff.modified, [])
def test_update_machine(self):
diff = diff_machines(
{
"changed": Machine(
cpus=1, disk_size=100, memory=1024, name="changed", active=True
),
},
{
"changed": Machine(
cpus=2, disk_size=100, memory=2048, name="changed", active=False
),
},
)
self.assertListEqual(diff.new, [])
self.assertListEqual(diff.same, [])
self.assertListEqual(diff.removed, [])
self.assertListEqual(
diff.modified,
[Machine(cpus=1, disk_size=100, memory=1024, name="changed", active=False)],
)
def test_remove_machine(self):
diff = diff_machines(
{
"same": Machine(
cpus=1, disk_size=100, memory=1024, name="same", active=True
),
},
{
"same": Machine(
cpus=1, disk_size=100, memory=1024, name="same", active=False
),
"removed": Machine(
cpus=1, disk_size=100, memory=1024, name="removed", active=True
),
},
)
self.assertListEqual(diff.new, [])
self.assertListEqual(
diff.same,
[Machine(cpus=1, disk_size=100, memory=1024, name="same", active=False)],
)
self.assertListEqual(
diff.removed,
[Machine(cpus=1, disk_size=100, memory=1024, name="removed", active=False)],
)
self.assertListEqual(diff.modified, [])
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,274 @@
{ config, lib, pkgs, ... }:
let
cfg = config.virtualisation.podman;
toml = pkgs.formats.toml { };
json = pkgs.formats.json { };
inherit (lib) mkDefault mkIf mkMerge mkOption types;
podmanPackage = (pkgs.podman.override { inherit (cfg) extraPackages; });
# Provides a fake "docker" binary mapping to podman
dockerAlias = pkgs.runCommandNoCC
"${podmanPackage.pname}-docker-alias-${podmanPackage.version}" {
outputs = [ "out" "man" ];
inherit (podmanPackage) meta;
} ''
mkdir -p $out/bin
ln -s ${podmanPackage}/bin/podman $out/bin/docker
mkdir -p $man/share/man/man1
for f in ${podmanPackage.man}/share/man/man1/*; do
basename=$(basename $f | sed s/podman/docker/g)
ln -s $f $man/share/man/man1/$basename
done
'';
podmactl = pkgs.callPackage ./podmactl { };
machineOpts = {
# Options here are loaded into python. For simplicity, please use
# snake_case.
options = {
active = mkOption {
type = types.bool;
default = false;
description = ''
This machine should be started. Only one machine can be active at a time
'';
};
qemu_binary = mkOption {
type = types.nullOr types.str;
default = null;
example = "''${pkgs.qemu}/bin/qemu-system-x86_64";
description = ''
Use this to start VM with the qemu appropriate for your architecture.
'';
};
# Options passed to Podman machine.
# See https://docs.podman.io/en/latest/markdown/podman-machine.1.html
cpus = mkOption {
type = types.ints.positive;
default = 1;
description = "The number of CPUs to assign to the VM.";
};
disk_size = mkOption {
type = types.ints.positive;
default = 100;
description = "Size of disk in gigabytes. Can only be increased";
};
image_path = mkOption {
type = types.nullOr types.str;
default = null;
example = lib.literalExpression ''
builtins.fetchurl "https://builds.coreos.fedoraproject.org/prod/streams/stable/builds/38.20230819.3.0/x86_64/fedora-coreos-38.20230819.3.0-qemu.x86_64.qcow2.xz"'';
description = ''
Image to be used when starting the VM
Can be a local path or a URL to an image.
Alternatives can be found at <https://fedoraproject.org/en/coreos/download>.
'';
};
memory = mkOption {
type = types.ints.positive;
default = 2048;
description = "RAM in MB to be assigned to the machine";
};
};
};
in {
meta.maintainers = [ pkgs.lib.maintainers.michaelCTS ];
options.virtualisation.podman = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This option enables Podman, a daemonless container engine for
developing, managing, and running OCI Containers on your Linux System.
It is a drop-in replacement for the {command}`docker` command.
'';
};
enableDockerSocket = mkOption {
type = types.bool;
default = false;
description = ''
Make the Podman socket available in place of the Docker socket, so
Docker tools can find the Podman socket.
Podman implements the Docker API.
'';
};
enableDockerAlias = mkOption {
type = types.bool;
default = false;
description = ''
Create an alias mapping {command}`docker` to {command}`podman`.
'';
};
extraPackages = mkOption {
type = with types; listOf package;
default = [ ];
example = lib.literalExpression "[ pkgs.gvisor ]";
description = ''
Extra packages to be installed in the Podman wrapper.
'';
};
finalPackage = lib.mkOption {
type = types.package;
internal = true;
readOnly = true;
default = podmanPackage;
description = ''
The final Podman package (including extra packages).
'';
};
defaultNetwork.extraPlugins = lib.mkOption {
type = types.listOf json.type;
default = [ ];
description = ''
Extra CNI plugin configurations to add to Podman's default network.
'';
};
machines = lib.mkOption {
type = types.attrsOf (types.submodule machineOpts);
# One and only one machine may be active at any given time
apply = machines:
assert ((lib.lists.count (machine: machine.active)
(lib.attrsets.attrValues machines)) == 1);
machines;
default = {
podman-machine-default = {
active = true;
cpus = 2;
disk_size = 100;
memory = 2048;
};
};
example = lib.literalExpression ''
{
intel-x86 = {
cpus = 2;
disk_size = 200;
memory = 4096;
image_path = "fedora-coreos-38.20230806.3.0-qemu.x86_64.qcow2.xz";
qemu_binary = "${pkgs.qemu}/bin/qemu-system-x86_64";
};
}
'';
description = ''
Virtual machine descriptions when Podman is run in on non-Linux systems.
'';
};
};
config = mkIf cfg.enable (mkMerge [
{
home.packages = [ cfg.finalPackage ]
++ lib.optional cfg.enableDockerAlias dockerAlias;
virtualisation.containers = {
enable = true; # Enable common /etc/containers configuration
};
}
(mkIf pkgs.stdenv.hostPlatform.isLinux (mkMerge [
{
systemd.user = {
services.podman = {
Unit = {
Description = "Podman API Service";
Requires = "podman.socket";
After = "podman.socket";
Documentation = "man:podman-system-service(1)";
StartLimitIntervalSec = 0;
};
Service = {
Type = "exec";
KillMode = "process";
Environment = ''LOGGING=" --log-level=info"'';
ExecStart = [
"${cfg.finalPackage}/bin/podman"
"$LOGGING"
"system"
"service"
];
};
Install = { WantedBy = [ "default.target" ]; };
};
sockets.podman = {
Unit = {
Description = "Podman API Socket";
Documentation = "man:podman-system-service(1)";
};
Socket = {
ListenStream = "%t/podman/podman.sock";
SocketMode = 660;
};
Install.WantedBy = [ "sockets.target" ];
};
};
}
(mkIf cfg.enableDockerSocket {
home.sessionVariables."DOCKER_HOST" =
"unix:///$XDG_RUNTIME_DIR/podman/podman.sock";
})
]))
(mkIf pkgs.stdenv.isDarwin (mkMerge [
{
home.packages = [
pkgs.qemu # To manage machines
pkgs.openssh # To ssh into the machines
];
}
{
home.extraActivationPath = [
pkgs.qemu # To manage machines.
pkgs.openssh # To ssh into the machines.
];
# CRUD the requested podman machines when activating the profile
home.activation.podman-machine =
lib.hm.dag.entryAfter [ "writeBoundary" ]
(lib.strings.concatStringsSep " " [
"$DRY_RUN_CMD"
"${podmactl}/bin/podmactl"
"--podman"
"${cfg.finalPackage}/bin/podman"
"$VERBOSE_ARG"
"${json.generate "podman-machines.json" cfg.machines}"
]);
}
# Socket is actually only available after the launchd agent has
# successfully completed and the machine has been started.
(mkIf cfg.enableDockerSocket {
home.sessionVariables."DOCKER_HOST" =
"unix:///Users/$USER/.local/share/containers/podman/machine/qemu/podman.sock";
})
]))
]);
}

View file

@ -159,6 +159,7 @@ import nmt {
./modules/programs/zplug
./modules/programs/zsh
./modules/services/syncthing/common
./modules/virtualisation/podman
./modules/xresources
] ++ lib.optionals isDarwin [
./modules/launchd
@ -263,6 +264,7 @@ import nmt {
./modules/services/wlsunset
./modules/services/xsettingsd
./modules/systemd
./modules/virtualisation/oci-containers
./modules/targets-linux
]);
}

View file

@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
lib.mkIf config.test.enableBig {
virtualisation.oci-containers.enable = true;
nmt.script = lib.mkIf pkgs.stdenv.isLinux ''
servicePath=home-files/.config/systemd/user
assertFileExists $servicePath/podman.service $servicePath/podman.socket
podmanServiceNormalized="$(normalizeStorePaths "$servicePath/podman.service")"
assertFileContent $podmanServiceNormalized \
${
builtins.toFile "podman.service-expected" ''
[Install]
WantedBy=default.target
[Service]
Environment=LOGGING=" --log-level=info"
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman
ExecStart=$LOGGING
ExecStart=system
ExecStart=service
KillMode=process
Type=exec
[Unit]
After=podman.socket
Description=Podman API Service
Documentation=man:podman-system-service(1)
Requires=podman.socket
StartLimitIntervalSec=0
''
}
assertFileContent $servicePath/podman.socket \
${
builtins.toFile "podman.socket-expected" ''
[Install]
WantedBy=sockets.target
[Socket]
ListenStream=%t/podman/podman.sock
SocketMode=660
[Unit]
Description=Podman API Socket
Documentation=man:podman-system-service(1)
''
}
'';
}

View file

@ -0,0 +1 @@
{ oci-containers-basic-config = ./basic-config.nix; }

View file

@ -0,0 +1,52 @@
{ config, pkgs, lib, ... }:
lib.mkIf config.test.enableBig {
virtualisation.podman.enable = true;
nmt.script = lib.mkIf pkgs.stdenv.isLinux ''
servicePath=home-files/.config/systemd/user
assertFileExists $servicePath/podman.service $servicePath/podman.socket
podmanServiceNormalized="$(normalizeStorePaths "$servicePath/podman.service")"
assertFileContent $podmanServiceNormalized \
${
builtins.toFile "podman.service-expected" ''
[Install]
WantedBy=default.target
[Service]
Environment=LOGGING=" --log-level=info"
ExecStart=/nix/store/00000000000000000000000000000000-podman/bin/podman
ExecStart=$LOGGING
ExecStart=system
ExecStart=service
KillMode=process
Type=exec
[Unit]
After=podman.socket
Description=Podman API Service
Documentation=man:podman-system-service(1)
Requires=podman.socket
StartLimitIntervalSec=0
''
}
assertFileContent $servicePath/podman.socket \
${
builtins.toFile "podman.socket-expected" ''
[Install]
WantedBy=sockets.target
[Socket]
ListenStream=%t/podman/podman.sock
SocketMode=660
[Unit]
Description=Podman API Socket
Documentation=man:podman-system-service(1)
''
}
'';
}

View file

@ -0,0 +1,4 @@
{
podman-basic-config = ./basic-config.nix;
podman-docker-alias = ./docker-alias.nix;
}

View file

@ -0,0 +1,14 @@
{ config, pkgs, lib, ... }:
lib.mkIf config.test.enableBig {
virtualisation.podman = {
enable = true;
enableDockerAlias = true;
enableDockerSocket = true;
};
nmt.script = ''
assertFileIsExecutable home-path/bin/docker
assertFileContains home-path/etc/profile.d/hm-session-vars.sh "DOCKER_HOST"
'';
}