Compare commits
1 commit
master
...
docker-ser
Author | SHA1 | Date | |
---|---|---|---|
faa4b16358 |
|
@ -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'.
|
||||
'';
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
76
modules/virtualisation/containers.nix
Normal file
76
modules/virtualisation/containers.nix
Normal 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";
|
||||
};
|
||||
|
||||
}
|
28
modules/virtualisation/oci-containers.nix
Normal file
28
modules/virtualisation/oci-containers.nix
Normal 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;
|
||||
};
|
||||
}
|
30
modules/virtualisation/podman/podmactl/README.md
Normal file
30
modules/virtualisation/podman/podmactl/README.md
Normal 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
|
||||
```
|
28
modules/virtualisation/podman/podmactl/default.nix
Normal file
28
modules/virtualisation/podman/podmactl/default.nix
Normal 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
|
||||
'';
|
||||
}
|
317
modules/virtualisation/podman/podmactl/podmactl.py
Normal file
317
modules/virtualisation/podman/podmactl/podmactl.py
Normal 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)
|
5
modules/virtualisation/podman/podmactl/shell.nix
Normal file
5
modules/virtualisation/podman/podmactl/shell.nix
Normal file
|
@ -0,0 +1,5 @@
|
|||
{ pkgs ? import <nixpkgs> { } }:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = [ (pkgs.python311.withPackages (ps: with ps; [ black ])) ];
|
||||
}
|
232
modules/virtualisation/podman/podmactl/test_podmactl.py
Normal file
232
modules/virtualisation/podman/podmactl/test_podmactl.py
Normal 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()
|
274
modules/virtualisation/podman/podman.nix
Normal file
274
modules/virtualisation/podman/podman.nix
Normal 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";
|
||||
})
|
||||
]))
|
||||
]);
|
||||
}
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
|
|
52
tests/modules/virtualisation/oci-containers/basic-config.nix
Normal file
52
tests/modules/virtualisation/oci-containers/basic-config.nix
Normal 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)
|
||||
''
|
||||
}
|
||||
'';
|
||||
}
|
1
tests/modules/virtualisation/oci-containers/default.nix
Normal file
1
tests/modules/virtualisation/oci-containers/default.nix
Normal file
|
@ -0,0 +1 @@
|
|||
{ oci-containers-basic-config = ./basic-config.nix; }
|
52
tests/modules/virtualisation/podman/basic-config.nix
Normal file
52
tests/modules/virtualisation/podman/basic-config.nix
Normal 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)
|
||||
''
|
||||
}
|
||||
'';
|
||||
}
|
4
tests/modules/virtualisation/podman/default.nix
Normal file
4
tests/modules/virtualisation/podman/default.nix
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
podman-basic-config = ./basic-config.nix;
|
||||
podman-docker-alias = ./docker-alias.nix;
|
||||
}
|
14
tests/modules/virtualisation/podman/docker-alias.nix
Normal file
14
tests/modules/virtualisation/podman/docker-alias.nix
Normal 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"
|
||||
'';
|
||||
}
|
Loading…
Reference in a new issue