services: Add rootless-podman-service-type.

* gnu/services/containers.scm: New file;
(rootless-podman-configuration): new variable;
(rootless-podman-service-subids): new variable;
(rootless-podman-service-accounts): new variable;
(rootless-podman-service-profile): new variable;
(rootless-podman-shepherd-services): new variable;
(rootless-podman-service-etc): new variable;
(rootless-podman-service-type): new variable.
* gnu/local.mk: Test it.
* gnu/local.mk: Add them.
* doc/guix.texi (Miscellaneous Services): Document it.

Change-Id: I041496474c1027da353bd6852f2554a065914d7a
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
This commit is contained in:
Giacomo Leidi 2024-08-23 13:40:57 +02:00 committed by Ludovic Courtès
parent 17e5ca8190
commit 2767b4ef03
No known key found for this signature in database
GPG key ID: 090B11993D9AEBB5
4 changed files with 690 additions and 0 deletions

View file

@ -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

View file

@ -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 \

238
gnu/services/containers.scm Normal file
View file

@ -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.")))

340
gnu/tests/containers.scm Normal file
View file

@ -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))))