From ef477374521a605a79355a7790e53e8a35dafc9d Mon Sep 17 00:00:00 2001 From: Michael Vogel Date: Fri, 11 Aug 2023 10:42:57 +0200 Subject: [PATCH] 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> --- modules/misc/news.nix | 10 + modules/modules.nix | 3 + modules/virtualisation/containers.nix | 76 +++++ modules/virtualisation/oci-containers.nix | 28 ++ modules/virtualisation/podman.nix | 279 ++++++++++++++++++ tests/default.nix | 2 + .../oci-containers/basic-config.nix | 52 ++++ .../virtualisation/oci-containers/default.nix | 1 + .../virtualisation/podman/basic-config.nix | 52 ++++ .../modules/virtualisation/podman/default.nix | 4 + .../virtualisation/podman/docker-alias.nix | 14 + 11 files changed, 521 insertions(+) create mode 100644 modules/virtualisation/containers.nix create mode 100644 modules/virtualisation/oci-containers.nix create mode 100644 modules/virtualisation/podman.nix create mode 100644 tests/modules/virtualisation/oci-containers/basic-config.nix create mode 100644 tests/modules/virtualisation/oci-containers/default.nix create mode 100644 tests/modules/virtualisation/podman/basic-config.nix create mode 100644 tests/modules/virtualisation/podman/default.nix create mode 100644 tests/modules/virtualisation/podman/docker-alias.nix diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 0b19711afd36..028ccd7e20d0 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1414,6 +1414,16 @@ in { A new module is available: 'programs.jetbrains-remote' ''; } + + { + time = "2023-02-14T08:45:52+00:00"; + message = '' + Three new modules are available: + 'virtualisation.containers', + 'virtualisation.oci-containers', + 'virtualisation.podman'. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index f8146efac8dc..70a2ddf59fdb 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -377,6 +377,9 @@ let ./systemd.nix ./targets/darwin ./targets/generic-linux.nix + ./virtualisation/containers.nix + ./virtualisation/oci-containers.nix + ./virtualisation/podman.nix ./xresources.nix ./xsession.nix ./misc/nix.nix diff --git a/modules/virtualisation/containers.nix b/modules/virtualisation/containers.nix new file mode 100644 index 000000000000..ac74767de064 --- /dev/null +++ b/modules/virtualisation/containers.nix @@ -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"; + }; + +} diff --git a/modules/virtualisation/oci-containers.nix b/modules/virtualisation/oci-containers.nix new file mode 100644 index 000000000000..fda69e26ccc2 --- /dev/null +++ b/modules/virtualisation/oci-containers.nix @@ -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; + }; +} diff --git a/modules/virtualisation/podman.nix b/modules/virtualisation/podman.nix new file mode 100644 index 000000000000..2271a17da45e --- /dev/null +++ b/modules/virtualisation/podman.nix @@ -0,0 +1,279 @@ +{ 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 = import (pkgs.fetchFromGitHub { + owner = "michaelCTS"; + repo = "podmactl"; + rev = "v0.0.4"; + hash = "sha256-7lMOcJZDkNSqBNXxI9iE0yn+hHhEwrWykS+C02NSQCk="; + }) { }; + + 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 . + ''; + }; + + 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"; + }) + ])) + ]); +} diff --git a/tests/default.nix b/tests/default.nix index be4d8f40a111..ab1de91b63ab 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -156,6 +156,7 @@ in import nmtSrc { ./modules/programs/zplug ./modules/programs/zsh ./modules/services/syncthing/common + ./modules/virtualisation/podman ./modules/xresources ] ++ lib.optionals isDarwin [ ./modules/launchd @@ -266,6 +267,7 @@ in import nmtSrc { ./modules/services/wob ./modules/services/xsettingsd ./modules/systemd + ./modules/virtualisation/oci-containers ./modules/targets-linux ]); } diff --git a/tests/modules/virtualisation/oci-containers/basic-config.nix b/tests/modules/virtualisation/oci-containers/basic-config.nix new file mode 100644 index 000000000000..ec998f0f67f2 --- /dev/null +++ b/tests/modules/virtualisation/oci-containers/basic-config.nix @@ -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) + '' + } + ''; +} diff --git a/tests/modules/virtualisation/oci-containers/default.nix b/tests/modules/virtualisation/oci-containers/default.nix new file mode 100644 index 000000000000..68ce9d9aa6f4 --- /dev/null +++ b/tests/modules/virtualisation/oci-containers/default.nix @@ -0,0 +1 @@ +{ oci-containers-basic-config = ./basic-config.nix; } diff --git a/tests/modules/virtualisation/podman/basic-config.nix b/tests/modules/virtualisation/podman/basic-config.nix new file mode 100644 index 000000000000..5b3355095584 --- /dev/null +++ b/tests/modules/virtualisation/podman/basic-config.nix @@ -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) + '' + } + ''; +} diff --git a/tests/modules/virtualisation/podman/default.nix b/tests/modules/virtualisation/podman/default.nix new file mode 100644 index 000000000000..6a738591e626 --- /dev/null +++ b/tests/modules/virtualisation/podman/default.nix @@ -0,0 +1,4 @@ +{ + podman-basic-config = ./basic-config.nix; + podman-docker-alias = ./docker-alias.nix; +} diff --git a/tests/modules/virtualisation/podman/docker-alias.nix b/tests/modules/virtualisation/podman/docker-alias.nix new file mode 100644 index 000000000000..4f5356608a90 --- /dev/null +++ b/tests/modules/virtualisation/podman/docker-alias.nix @@ -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" + ''; +}