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" + ''; +}