diff options
-rw-r--r-- | doc/guix.texi | 110 | ||||
-rw-r--r-- | gnu/local.mk | 2 | ||||
-rw-r--r-- | gnu/services/containers.scm | 238 | ||||
-rw-r--r-- | gnu/tests/containers.scm | 340 |
4 files changed, 690 insertions, 0 deletions
diff --git a/doc/guix.texi b/doc/guix.texi index fe84b52052..ee2002a712 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -41292,6 +41292,116 @@ setuid-root (@pxref{Privileged Programs}) such that unprivileged users can invoke @command{singularity run} and similar commands. @end defvar +@cindex Rootless Podman +@subsubheading Rootless Podman Service + +@cindex rootless podman, container management tool +@cindex podman, rootless +@cindex container management, podman +The @code{(gnu services containers)} module provides the following service. + + +@defvar rootless-podman-service-type +This is the service type for @url{https://podman.io, Podman} is a +container management tool. + +In addition to providing a drop-in replacement for Docker, Podman offers +the ability to run containers in ``root-less'' mode, meaning that regular users can +deploy containers without elevated privileges. It does so mainly by leveraging +two Linux kernel features: unprivileged user namespaces, and subordinate +user and group IDs (@pxref{subordinate-user-group-ids, the subordinate +user and group ID service}). + +The @code{rootless-podman-service-type} sets up the system to allow +unprivileged users to run @command{podman} commands: + +@lisp +(use-service-modules containers networking @dots{}) + +(operating-system + ;; @dots{} + (users (cons (user-account + (name "alice") + (comment "Bob's sister") + (group "users") + + ;; Adding the account to the "cgroup" group + ;; makes it possible to run podman commands. + (supplementary-groups '("cgroup" "wheel" + "audio" "video"))) + %base-user-accounts)) + (services + (append (list (service iptables-service-type) + (service rootless-podman-service-type + (rootless-podman-configuration + (subgids + (list (subid-range (name "alice")))) + (subuids + (list (subid-range (name "alice"))))))) + %base-services))) +@end lisp + +The @code{iptables-service-type} is required for Podman to be able to setup its +own networks. Due to the change in user groups and file systems it is +recommended to reboot (or at least logout), before trying to run Podman commands. + +To test your installation you can run: + +@example +$ podman run -it --rm docker.io/alpine cat /etc/*release* +NAME="Alpine Linux" +ID=alpine +VERSION_ID=3.20.2 +PRETTY_NAME="Alpine Linux v3.20" +HOME_URL="https://alpinelinux.org/" +BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues" +@end example + +@end defvar + +@c %start of fragment + +@deftp {Data Type} rootless-podman-configuration +Available @code{rootless-podman-configuration} fields are: + +@table @asis +@item @code{podman} (default: @code{podman}) (type: package) +The Podman package that will be installed in the system profile. + +@item @code{group-name} (default: @code{"cgroup"}) (type: string) +The name of the group that will own /sys/fs/cgroup resources. Users that +want to use rootless Podman have to be in this group. + +@item @code{containers-registries} (type: lowerable) +A string or a gexp evaluating to the path of Podman's +@code{containers/registries.conf} configuration file. + +@item @code{containers-storage} (type: lowerable) +A string or a gexp evaluating to the path of Podman's +@code{containers/storage.conf} configuration file. + +@item @code{containers-policy} (type: lowerable) +A string or a gexp evaluating to the path of Podman's +@code{containers/policy.json} configuration file. + +@item @code{pam-limits} (type: list-of-pam-limits-entries) +The PAM limits to be set for rootless Podman. + +@item @code{subgids} (default: @code{()}) (type: list-of-subid-ranges) +A list of subid ranges representing the subgids that will be +available for each configured user. + +@item @code{subuids} (default: @code{()}) (type: list-of-subid-ranges) +A list of subid ranges representing the subuids that will be +available for each configured user. + +@end table + +@end deftp + + +@c %end of fragment + @cindex OCI-backed, Shepherd services @subsubheading OCI backed services diff --git a/gnu/local.mk b/gnu/local.mk index d5be5e0198..e99b00ed93 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -719,6 +719,7 @@ GNU_SYSTEM_MODULES = \ %D%/services/cgit.scm \ %D%/services/ci.scm \ %D%/services/configuration.scm \ + %D%/services/containers.scm \ %D%/services/cuirass.scm \ %D%/services/cups.scm \ %D%/services/databases.scm \ @@ -825,6 +826,7 @@ GNU_SYSTEM_MODULES = \ %D%/tests/base.scm \ %D%/tests/cachefilesd.scm \ %D%/tests/ci.scm \ + %D%/tests/containers.scm \ %D%/tests/cups.scm \ %D%/tests/databases.scm \ %D%/tests/desktop.scm \ diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm new file mode 100644 index 0000000000..03f0649c0d --- /dev/null +++ b/gnu/services/containers.scm @@ -0,0 +1,238 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2024 Giacomo Leidi <goodoldpaul@autistici.org> +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. + +(define-module (gnu services containers) + #:use-module (gnu packages containers) + #:use-module (gnu packages file-systems) + #:use-module (gnu services) + #:use-module (gnu services base) + #:use-module (gnu services configuration) + #:use-module (gnu services shepherd) + #:use-module (gnu system accounts) + #:use-module (gnu system shadow) + #:use-module (gnu system pam) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (rootless-podman-configuration + rootless-podman-configuration? + rootless-podman-configuration-fields + rootless-podman-configuration-podman + rootless-podman-configuration-group-name + rootless-podman-configuration-containers-registries + rootless-podman-configuration-containers-storage + rootless-podman-configuration-containers-policy + rootless-podman-configuration-pam-limits + rootless-podman-configuration-subgids + rootless-podman-configuration-subuids + + rootless-podman-service-subids + rootless-podman-service-accounts + rootless-podman-service-profile + rootless-podman-shepherd-services + rootless-podman-service-etc + + rootless-podman-service-type)) + +(define (gexp-or-string? value) + (or (gexp? value) + (string? value))) + +(define (lowerable? value) + (or (file-like? value) + (gexp-or-string? value))) + +(define list-of-pam-limits-entries? + (list-of pam-limits-entry?)) + +(define list-of-subid-ranges? + (list-of subid-range?)) + +(define-configuration/no-serialization rootless-podman-configuration + (podman + (package podman) + "The Podman package that will be installed in the system profile.") + (group-name + (string "cgroup") + "The name of the group that will own /sys/fs/cgroup resources. Users that +want to use rootless Podman have to be in this group.") + (containers-registries + (lowerable + (plain-file "registries.conf" + (string-append "unqualified-search-registries = ['docker.io','" + "registry.fedora.org','registry.opensuse.org']"))) + "A string or a gexp evaluating to the path of Podman's +@code{containers/registries.conf} configuration file.") + (containers-storage + (lowerable + (plain-file "storage.conf" + "[storage] +driver = \"overlay\"")) + "A string or a gexp evaluating to the path of Podman's +@code{containers/storage.conf} configuration file.") + (containers-policy + (lowerable + (plain-file "policy.json" + "{\"default\": [{\"type\": \"insecureAcceptAnything\"}]}")) + "A string or a gexp evaluating to the path of Podman's +@code{containers/policy.json} configuration file.") + (pam-limits + (list-of-pam-limits-entries + (list (pam-limits-entry "*" 'both 'nofile 100000))) + "The PAM limits to be set for rootless Podman.") + (subgids + (list-of-subid-ranges '()) + "A list of subid ranges representing the subgids that will be +available for each configured user.") + (subuids + (list-of-subid-ranges '()) + "A list of subid ranges representing the subuids that will be +available for each configured user.")) + +(define rootless-podman-service-profile + (lambda (config) + (list + (rootless-podman-configuration-podman config)))) + +(define rootless-podman-service-etc + (lambda (config) + (list `("containers/registries.conf" + ,(rootless-podman-configuration-containers-registries config)) + `("containers/storage.conf" + ,(rootless-podman-configuration-containers-storage config)) + `("containers/policy.json" + ,(rootless-podman-configuration-containers-policy config))))) + +(define rootless-podman-service-subids + (lambda (config) + (subids-extension + (subgids (rootless-podman-configuration-subgids config)) + (subuids (rootless-podman-configuration-subuids config))))) + +(define rootless-podman-service-accounts + (lambda (config) + (list (user-group (name (rootless-podman-configuration-group-name config)) + (system? #t))))) + +(define (cgroups-fs-owner-entrypoint config) + (define group + (rootless-podman-configuration-group-name config)) + (program-file "cgroups2-fs-owner-entrypoint" + #~(system* + "bash" "-c" + (string-append "echo Setting /sys/fs/cgroup " + "group ownership to " #$group " && chown -v " + "root:" #$group " /sys/fs/cgroup && " + "chmod -v 775 /sys/fs/cgroup && chown -v " + "root:" #$group " /sys/fs/cgroup/cgroup." + "{procs,subtree_control,threads} && " + "chmod -v 664 /sys/fs/cgroup/cgroup." + "{procs,subtree_control,threads}")))) + +(define (rootless-podman-cgroups-fs-owner-service config) + (shepherd-service (provision '(cgroups2-fs-owner)) + (requirement + '(dbus-system + elogind + file-system-/sys/fs/cgroup + networking + udev + cgroups2-limits)) + (one-shot? #t) + (documentation + "Set ownership of /sys/fs/cgroup to the configured group.") + (start + #~(make-forkexec-constructor + (list + #$(cgroups-fs-owner-entrypoint config)))) + (stop + #~(make-kill-destructor)))) + +(define cgroups-limits-entrypoint + (program-file "cgroups2-limits-entrypoint" + #~(system* + "bash" "-c" + (string-append "echo Setting cgroups v2 limits && " + "echo +cpu +cpuset +memory +pids" + " >> /sys/fs/cgroup/cgroup.subtree_control")))) + +(define (rootless-podman-cgroups-limits-service config) + (shepherd-service (provision '(cgroups2-limits)) + (requirement + '(dbus-system + elogind + networking + udev + file-system-/sys/fs/cgroup + rootless-podman-shared-root-fs)) + (one-shot? #t) + (documentation + "Allow setting cgroups limits: cpu, cpuset, memory and +pids.") + (start + #~(make-forkexec-constructor + (list + #$cgroups-limits-entrypoint))) + (stop + #~(make-kill-destructor)))) + +(define rootless-podman-shared-root-fs-entrypoint + (program-file "rootless-podman-shared-root-fs-entrypoint" + #~(system* + "mount" "--make-shared" "/"))) + +(define (rootless-podman-shared-root-fs-service config) + (shepherd-service (provision '(rootless-podman-shared-root-fs)) + (requirement + '(user-processes)) + (one-shot? #t) + (documentation + "Buildah/Podman running as rootless expects the bind mount +to be shared. This service sets it so.") + (start + #~(make-forkexec-constructor + (list + #$rootless-podman-shared-root-fs-entrypoint))) + (stop + #~(make-kill-destructor)))) + +(define (rootless-podman-shepherd-services config) + (list + (rootless-podman-shared-root-fs-service config) + (rootless-podman-cgroups-limits-service config) + (rootless-podman-cgroups-fs-owner-service config))) + +(define rootless-podman-service-type + (service-type (name 'rootless-podman) + (extensions + (list + (service-extension subids-service-type + rootless-podman-service-subids) + (service-extension account-service-type + rootless-podman-service-accounts) + (service-extension profile-service-type + rootless-podman-service-profile) + (service-extension shepherd-root-service-type + rootless-podman-shepherd-services) + (service-extension pam-limits-service-type + rootless-podman-configuration-pam-limits) + (service-extension etc-service-type + rootless-podman-service-etc))) + (default-value (rootless-podman-configuration)) + (description + "This service configures rootless @code{podman} on the Guix System."))) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm new file mode 100644 index 0000000000..ba2fb22df6 --- /dev/null +++ b/gnu/tests/containers.scm @@ -0,0 +1,340 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2024 Giacomo Leidi <goodoldpaul@autistici.org> +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. + +(define-module (gnu tests containers) + #:use-module (gnu) + #:use-module (gnu tests) + #:use-module (guix build-system trivial) + #:use-module (gnu packages bash) + #:use-module (gnu packages containers) + #:use-module (gnu packages guile) + #:use-module (gnu packages guile-xyz) + #:use-module (gnu services) + #:use-module (gnu services containers) + #:use-module (gnu services desktop) + #:use-module (gnu services dbus) + #:use-module (gnu services networking) + #:use-module (gnu system) + #:use-module (gnu system accounts) + #:use-module (gnu system vm) + #:use-module (guix gexp) + #:use-module ((guix licenses) #:prefix license:) + #:use-module (guix monads) + #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) + #:export (%test-rootless-podman)) + + +(define %rootless-podman-os + (simple-operating-system + (service rootless-podman-service-type + (rootless-podman-configuration + (subgids + (list (subid-range (name "dummy")))) + (subuids + (list (subid-range (name "dummy")))))) + + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + + (simple-service 'accounts + account-service-type + (list (user-account + (name "dummy") + (group "users") + (supplementary-groups '("wheel" "netdev" "cgroup" + "audio" "video"))))))) + +(define (run-rootless-podman-test oci-tarball) + + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %rootless-podman-os + (list oci-tarball)) + #:imported-modules '((gnu services herd) + (guix combinators)))) + + (define vm + (virtual-machine + (operating-system os) + (volatile? #f) + (memory-size 1024) + (disk-image-size (* 3000 (expt 2 20))) + (port-forwardings '()))) + + (define test + (with-imported-modules '((gnu build marionette) + (gnu services herd)) + #~(begin + (use-modules (srfi srfi-11) (srfi srfi-64) + (gnu build marionette)) + + (define marionette + ;; Relax timeout to accommodate older systems and + ;; allow for pulling the image. + (make-marionette (list #$vm) #:timeout 60)) + (define out-dir "/tmp") + + (test-runner-current (system-test-runner #$output)) + (test-begin "rootless-podman") + + (test-assert "service started" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (match (start-service 'cgroups2-fs-owner) + (#f #f) + ;; herd returns (running #f), likely because of one shot, + ;; so consider any non-error a success. + (('service response-parts ...) #t))) + marionette)) + + (test-equal "/sys/fs/cgroup/cgroup.subtree_control content is sound" + (list "cpu" "cpuset" "memory" "pids") + (marionette-eval + `(begin + (use-modules (srfi srfi-1) + (ice-9 popen) + (ice-9 match) + (ice-9 rdelim)) + + (define (read-lines file-or-port) + (define (loop-lines port) + (let loop ((lines '())) + (match (read-line port) + ((? eof-object?) + (reverse lines)) + (line + (loop (cons line lines)))))) + + (if (port? file-or-port) + (loop-lines file-or-port) + (call-with-input-file file-or-port + loop-lines))) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (read-lines port)) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$coreutils "/bin/cat") + "/sys/fs/cgroup/cgroup.subtree_control"))) + (sort-list (string-split (first response1) #\space) string<?))) + marionette)) + + (test-equal "/sys/fs/cgroup has correct permissions" + '("cgroup" "cgroup") + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 match) + (ice-9 rdelim)) + + (define (read-lines file-or-port) + (define (loop-lines port) + (let loop ((lines '())) + (match (read-line port) + ((? eof-object?) + (reverse lines)) + (line + (loop (cons line lines)))))) + + (if (port? file-or-port) + (loop-lines file-or-port) + (call-with-input-file file-or-port + loop-lines))) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (read-lines port)) + (status (close-pipe port))) + output))) + (let* ((bash + ,(string-append #$bash "/bin/bash")) + (response1 + (slurp bash "-c" + (string-append "ls -la /sys/fs/cgroup | " + "grep -E ' \\./?$' | awk '{ print $4 }'"))) + (response2 (slurp bash "-c" + (string-append "ls -l /sys/fs/cgroup/cgroup" + ".{procs,subtree_control,threads} | " + "awk '{ print $4 }' | sort -u")))) + (list (string-join response1 "\n") (string-join response2 "\n")))) + marionette)) + + (test-equal "Load oci image and run it (unprivileged)" + '("hello world" "hi!" "JSON!" #o1777) + (marionette-eval + `(begin + (use-modules (srfi srfi-1) + (ice-9 popen) + (ice-9 match) + (ice-9 rdelim)) + + (define (wait-for-file file) + ;; Wait until FILE shows up. + (let loop ((i 60)) + (cond ((file-exists? file) + #t) + ((zero? i) + (error "file didn't show up" file)) + (else + (pk 'wait-for-file file) + (sleep 1) + (loop (- i 1)))))) + + (define (read-lines file-or-port) + (define (loop-lines port) + (let loop ((lines '())) + (match (read-line port) + ((? eof-object?) + (reverse lines)) + (line + (loop (cons line lines)))))) + + (if (port? file-or-port) + (loop-lines file-or-port) + (call-with-input-file file-or-port + loop-lines))) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (match (primitive-fork) + (0 + (dynamic-wind + (const #f) + (lambda () + (setgid (passwd:gid (getpwnam "dummy"))) + (setuid (passwd:uid (getpw "dummy"))) + + (let* ((loaded (slurp ,(string-append #$podman + "/bin/podman") + "load" "-i" + ,#$oci-tarball)) + (repository&tag "localhost/guile-guest:latest") + (response1 (slurp + ,(string-append #$podman "/bin/podman") + "run" "--pull" "never" + "--entrypoint" "bin/Guile" + repository&tag + "/aa.scm")) + (response2 (slurp ;default entry point + ,(string-append #$podman "/bin/podman") + "run" "--pull" "never" repository&tag + "-c" "'(display \"hi!\")'")) + + ;; Check whether (json) is in $GUILE_LOAD_PATH. + (response3 (slurp ;default entry point + environment + ,(string-append #$podman "/bin/podman") + "run" "--pull" "never" repository&tag + "-c" "'(use-modules (json)) + (display (json-string->scm (scm->json-string \"JSON!\")))'")) + + ;; Check whether /tmp exists. + (response4 (slurp + ,(string-append #$podman "/bin/podman") + "run" "--pull" "never" repository&tag "-c" + "'(display (stat:perms (lstat \"/tmp\")))'"))) + (call-with-output-file (string-append ,out-dir "/response1") + (lambda (port) + (display (string-join response1 " ") port))) + (call-with-output-file (string-append ,out-dir "/response2") + (lambda (port) + (display (string-join response2 " ") port))) + (call-with-output-file (string-append ,out-dir "/response3") + (lambda (port) + (display (string-join response3 " ") port))) + (call-with-output-file (string-append ,out-dir "/response4") + (lambda (port) + (display (string-join response4 " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + (wait-for-file (string-append ,out-dir "/response4")) + (append + (slurp "cat" (string-append ,out-dir "/response1")) + (slurp "cat" (string-append ,out-dir "/response2")) + (slurp "cat" (string-append ,out-dir "/response3")) + (map string->number (slurp "cat" (string-append ,out-dir "/response4"))))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-test" test)) + +(define (build-tarball&run-rootless-podman-test) + (mlet* %store-monad + ((_ (set-grafting #f)) + (guile (set-guile-for-build (default-guile))) + (guest-script-package -> + (package + (name "guest-script") + (version "0") + (source #f) + (build-system trivial-build-system) + (arguments `(#:guile ,guile-3.0 + #:builder + (let ((out (assoc-ref %outputs "out"))) + (mkdir out) + (call-with-output-file (string-append out "/a.scm") + (lambda (port) + (display "(display \"hello world\n\")" port))) + #t))) + (synopsis "Display hello world using Guile") + (description "This package displays the text \"hello world\" on the +standard output device and then enters a new line.") + (home-page #f) + (license license:public-domain))) + (profile (profile-derivation (packages->manifest + (list guile-3.0 guile-json-3 + guest-script-package)) + #:hooks '() + #:locales? #f)) + (tarball (pack:docker-image + "docker-pack" profile + #:symlinks '(("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm")) + #:extra-options + '(#:image-tag "guile-guest") + #:entry-point "bin/guile" + #:localstatedir? #t))) + (run-rootless-podman-test tarball))) + +(define %test-rootless-podman + (system-test + (name "rootless-podman") + (description "Test rootless Podman service.") + (value (build-tarball&run-rootless-podman-test)))) |