Subject: [bug#72740] [PATCH v2 4/4] services: Add rootless-podman-service-type.

From: Giacomo Leidi <[email-masked]>
Date: Wed, 21 Aug 2024 10:19:27 +0200

* 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
---
 doc/guix.texi               | 104 +++++++++++
 gnu/local.mk                |   2 +
 gnu/services/containers.scm | 238 +++++++++++++++++++++++++
 gnu/tests/containers.scm    | 340 ++++++++++++++++++++++++++++++++++++
 4 files changed, 684 insertions(+)
 create mode 100644 gnu/services/containers.scm
 create mode 100644 gnu/tests/containers.scm

diff --git a/doc/guix.texi b/doc/guix.texi
index 0e1e253b02..eb6a1b2442 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -40852,6 +40852,110 @@ Miscellaneous Services
 invoke @command{singularity run} and similar commands.
 @end defvar
 
+@cindex Rootless Podman
+@subsubheading Rootless Podman Service
+
+The @code{(gnu services containers)} module provides the following service.
+
+
+@cindex Rootless Podman, container management tool
+@defvar rootless-podman-service-type
+
+@url{https://www.sylabs.io/singularity/, Singularity} is a container management
+tool.  In addition to providing a drop-in replacement for Docker, Podman offers
+the ability to run containers in rootless mode.  This allows regular users to
+deploy containers without elevated privileges.
+
+The @code{rootless-podman-service-type} sets up the Guix 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
+    (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")))))))))
+@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 3b0a3858f7..a543f1ddc9 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -708,6 +708,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			\
@@ -813,6 +814,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 <[email-masked]>
+;;;
+;;; 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 <[email-masked]>
+;;;
+;;; 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))))
-- 
2.45.2