Package: guix-patches;
Reported by: paul <goodoldpaul <at> autistici.org>
Date: Wed, 5 Feb 2025 22:01:03 UTC
Severity: normal
Tags: moreinfo
To reply to this bug, email your comments to 76081 AT debbugs.gnu.org.
Toggle the display of automated, internal messages from the tracker.
View this report as an mbox folder, status mbox, maintainer mbox
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 05 Feb 2025 22:01:03 GMT) Full text and rfc822 format available.paul <goodoldpaul <at> autistici.org>
:guix-patches <at> gnu.org
.
(Wed, 05 Feb 2025 22:01:03 GMT) Full text and rfc822 format available.Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: guix-patches <at> gnu.org Subject: OCI provisioning service Date: Wed, 5 Feb 2025 22:59:57 +0100
[Message part 1 (text/plain, inline)]
Hi Guix, for a while now, we have been able to run Docker/OCI images as Shepherd services with the oci-container-service-type. This was useful especially to run software that is not packaged yet (or sometimes not packageable at all, due to JS and other bootstrapping issues). It allows to declare a list of oci-container-configuration records which would map to Docker backed Shepherd services: (service oci-container-service-type (list (oci-container-configuration (image "prom/prometheus") (network "host") (ports '(("9000" . "9000") ("9090" . "9090")))) (oci-container-configuration (image "grafana/grafana:10.0.1") (network "host") (volumes '("/var/lib/grafana:/var/lib/grafana"))))) This allows us to have containerized but apparently native services running on the Guix System. The downside is that unless some isolation mechanism are disabled, or unless the running services are very simple and don't have to interact with the world outside them, we lose some of the nice virtualization features of containers. Above all is the need to have all containers connected to the host network to be able to interact with other containers. This is effectively the default behavior of most if not all Guix native services but with the downside that usually OCI images lack all the provenance information that is typical of Guix packages, hence are less trustable than a native Guix package so in some cases users do prefer to have them running in an isolated environment. Another shortcoming of the oci-container-service-type is that it only supports Docker as an OCI runtime which must have a running daemon with full root privileges to be able to execute containers. Since some weeks now, with the help of subids/subgids and unprivileged namespaces, we are able to run completely rootless containers with the rootless-podman-service-type. This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes: (service iptables-service-type) (service rootless-podman-service-type) (service oci-service-type (oci-configuration (runtime 'podman))) (simple-service 'oci-provisioning oci-service-type (oci-extension (volumes (list (oci-network-configuration (name "grafana")))) (networks (list (oci-network-configuration (name "monitoring")))) (containers (list (oci-container-configuration (image "docker.io/grafana/grafana:10.1.5") (network "host") (volumes `(,( . "/opt/bitnami/grafana/conf/grafana.ini") ("grafana" . "/var/lib/grafana")))))))) Please mind that this is only an example and it probably won't work on your system without some more thought. The oci-service-type currently only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. Compared to the oci-container-service-type I added some utility Shepherd actions that are supposed to help with debugging. The above configuration would yield the following $ sudo herd command-line podman-networks /run/current-system/profile/bin/podman network create monitoring $ sudo herd command-line podman-volumes /run/current-system/profile/bin/podman volume create grafana $ sudo herd doc podman-grafana list-actions command-line: Prints podman-grafana's OCI runtime command line invokation. pull: Pull podman-grafana's image (docker.io/grafana/grafana:10.1.5). $ sudo herd command-line podman-grafana /run/current-system/profile/bin/podman run --rm --name podman-grafana --network host -v grafana:/var/lib/grafana -v /gnu/store/yqfvhvf8j4008ykr52zh2dmc1d2mjxih-grafana.ini:/opt/bitnami/grafana/conf/grafana.ini docker.io/bitnami/grafana:10.1.5 At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future (you can find an untested version of that at [0]). I tested my changes with: guix shell -D guix -CPW -- make check-system TESTS="oci-service-docker oci-container oci-service-rootless-podman docker docker-system rootless-podman" Please let me know your thoughts about this! Thank you for all your work, giacomo [0]: https://github.com/fishinthecalculator/gocix/blob/main/modules/oci/home/services/containers.scm
[Message part 2 (text/html, inline)]
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 05 Feb 2025 22:04:02 GMT) Full text and rfc822 format available.Message #8 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH 1/4] services: rootless-podman: Use login shell. Date: Wed, 5 Feb 2025 23:02:28 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 19d35ccbcb6..dc66ac4f967 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 5a897c5c95a81278b044c18d962d3bd83131ba06 -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 05 Feb 2025 22:04:02 GMT) Full text and rfc822 format available.Message #11 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH 4/4] tests: Use oci-image in container tests. Date: Wed, 5 Feb 2025 23:02:31 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++---------------- gnu/tests/docker.scm | 98 ++++++++++++++++++++----------------- 3 files changed, 93 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index c45f79c4ed1..e15dbc6a21c 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -74,6 +74,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 719647c298e..024000bca14 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image + "guile-guest" + (oci-image + (repository "guile-guest") + (value + (packages->manifest + (list + guile-3.0 guile-json-3 + (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))))) + (pack-options + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))))))) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %guile-oci-image diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..d969b28a68f 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image + "guile-guest" + (oci-image + (repository "guile-guest") + (value + (packages->manifest + (list + guile-3.0 guile-json-3 + (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))))) + (pack-options + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))))))) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,20 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image + "guix-system-guest" + (oci-image + (repository "guix-system-guest") + (value + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))))))) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +347,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 05 Feb 2025 22:04:03 GMT) Full text and rfc822 format available.Message #14 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH 3/4] services: Add oci-service-type. Date: Wed, 5 Feb 2025 23:02:30 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 295 +++++++--- gnu/services/containers.scm | 1038 +++++++++++++++++++++++++++++++---- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 956 +++++++++++++++++++++++++++++++- 4 files changed, 2130 insertions(+), 196 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index bb5f29277fb..ff3e77a1d00 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -41778,59 +41778,155 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. + +@item @code{user} (default: @code{"oci-container"}) (type: string) +The user name under whose authority OCI commands will be run. + +@item @code{group} (default: @code{"docker"}) (type: string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -41850,16 +41946,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -41867,22 +41963,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -41910,7 +42008,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -41921,10 +42019,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -41935,25 +42034,95 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly +passed to the runtime invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +passed to the runtime invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 50bf6c05549..c45f79c4ed1 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -41,6 +41,7 @@ (define-module (gnu services containers) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +97,68 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-fields + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-networks-shepherd-name + oci-volumes-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services)) (define (gexp-or-string? value) (or (gexp? value) @@ -294,9 +355,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs) + '(dockerd))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (maybe-value-set? maybe-group) + maybe-group + (if (eq? 'podman runtime) + "cgroup" + "docker"))) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -345,6 +439,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -372,6 +471,7 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) + "Return a string OCI image reference representing IMAGE." (if (string? image) image (string-append (oci-image-repository image) @@ -390,7 +490,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -436,10 +548,12 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user (string "oci-container") - "The user under whose authority docker commands will be run.") + "The user name under whose authority OCI commands will be run.") (group (string "docker") - "The group under whose authority docker commands will be run.") + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -449,9 +563,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -475,15 +589,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -512,7 +627,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -524,9 +639,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -539,63 +655,307 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invokation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the runtime invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the runtime invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-configuration/no-serialization oci-configuration + (runtime + (symbol 'docker) + "The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}." + (sanitizer oci-sanitize-runtime)) + (runtime-cli + (maybe-package) + "The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime.") + (user + (string "oci-container") + "The user name under whose authority OCI runtime commands will be run.") + (group + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") + (subuids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (subgids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (verbose? + (boolean #f) + "When true, additional output will be printed, allowing to better follow the +flow of execution.")) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + (if #$(maybe-value-set? runtime-cli) + #$runtime-cli + #$path) + (if #$(eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invokation) + "Return a Shepherd action printing a given INVOKATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invokation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invokation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define* (get-keyword-value args keyword #:key (default #f)) (let ((kv (memq keyword args))) @@ -604,6 +964,7 @@ (define* (get-keyword-value args keyword #:key (default #f)) default))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -613,6 +974,7 @@ (define (lower-operating-system os target system) (return tarball))) (define (lower-manifest name image target system) + "Lower IMAGE, a manifest record, into a tarball containing an OCI image." (define value (oci-image-value image)) (define options (oci-image-pack-options image)) (define image-reference @@ -645,6 +1007,7 @@ (define (lower-manifest name image target system) (return tarball))) (define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." (define value (oci-image-value image)) (define image-target (oci-image-target image)) (define image-system (oci-image-system image)) @@ -675,9 +1038,10 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define* (oci-image-loader runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) (program-file (format #f "~a-image-loader" name) #~(begin @@ -686,102 +1050,536 @@ (define (%oci-image-loader name image tag) (ice-9 rdelim)) (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + (when #$verbose? + (format #t "Running ~a~%" load-command)) (define line (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) + (open-input-pipe load-command))) (unless (or (eof-object? line) (string-null? line)) (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag))) + + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) - (invoke #$docker "tag" repository&tag #$tag) + (apply invoke tag-command) (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) +(define* (oci-container-entrypoint runtime-cli name image image-reference + invokation #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format)) + (when #$verbose? + (format #t "Running in verbose mode...")) + (define invokation (list #$@invokation)) + #$@(if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime-cli name image + image-reference #:verbose? verbose?))) + #~()) + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (apply execlp invokation)))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (user (or user (oci-container-configuration-user config))) + (group (if (and group (maybe-value-set? group)) + group + (oci-container-configuration-group config))) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invokation + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,runtime-cli "run" + "--rm" "--name" ,name + ,@options ,@extra-arguments + ,image-reference ,@command))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@(oci-runtime-system-requirement runtime) + user-processes + ,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) + #~(lambda _ (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) + (list + #$(oci-container-entrypoint + runtime-cli name image image-reference + invokation #:verbose? verbose?)) #:user #$user - #:group #$group + #:group + (if #$(eq? runtime 'podman) + (group:name + (getgrgid + (passwd:gid (getpwnam #$user)))) + #$group) #$@(if (maybe-value-set? log-file) (list #:log-file log-file) '()) #:environment-variables - (list #$@host-environment)))) + (append + (list #$@host-environment) + (if #$(eq? runtime 'podman) + (list + (string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + '()))))) (stop #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (invoke #$runtime-cli "rm" "-f" #$name))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (cdr (list #$@invokation)) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + #~(lambda _ + (invoke #$runtime-cli "pull" #$image))))))) + actions))))) + +(define (oci-object-create-invokation object runtime-cli name options + extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invokation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$object "create" + #$@options #$@extra-arguments #$name)) + +(define (format-oci-invokations invokations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOKATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invokations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invokations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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 (object-exists? name) + (if (string=? #$runtime-string "podman") + (let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + (let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"{{.Name}}\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command)))))) + + (for-each + (lambda (invokation) + (define name (last invokation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (let ((exit-code (status:exit-val (apply system* invokation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invokations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invokations + #:key + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOKATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(user-processes ,@requirement)) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invokations + #:verbose? verbose?)) + #:user #$user + #:group + (if #$(eq? runtime 'podman) + (group:name + (getgrgid + (passwd:gid (getpwnam #$user)))) + #$group) + #$@(if (eq? runtime 'podman) + (list + #:environment-variables + #~(list + (string-append + "HOME=" (passwd:dir (getpwnam #$user))))) + '())))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invokations invokations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key + (user #f) + (group #f) + (runtime-requirement '()) + (default-requirement '(networking)) + (verbose? #f)) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invokations + (map + (lambda (network) + (oci-object-create-invokation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + (append default-requirement runtime-requirement) invokations + #:user user #:group group #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invokations + (map + (lambda (volume) + (oci-object-create-invokation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invokations + #:user user #:group group #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-requirement '()) + (networks-default-requirement '())) + (let* ((networks? + (> (length networks) 0)) + (networks-requirement + (if networks? + (list + (string->symbol + (oci-networks-shepherd-name runtime))) + '())) + (volumes? + (> (length volumes) 0)) + (volumes-requirement + (if volumes? + (list + (string->symbol + (oci-volumes-shepherd-name runtime))) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:oci-requirement + (append networks-requirement volumes-requirement) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime)) + networks + #:user user #:group group + #:default-requirement networks-default-requirement + #:runtime-requirement runtime-requirement + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service runtime runtime-cli + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime)) + volumes + #:user user #:group group + #:runtime-requirement runtime-requirement + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (runtime-cli + (oci-runtime-system-cli config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config))) + (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:user user #:group group #:verbose? verbose? + #:runtime-requirement + (oci-runtime-system-requirement runtime)))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) (not (string=? (subid-range-name range) user))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subgids) + subgids + (subid-range (name user))) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subuids) + subuids + (subid-range (name user))) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-networks-shepherd-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volumes-shepherd-name)))) + +(define (oci-service-profile runtime runtime-cli) + (list bash-minimal + (cond + ((maybe-value-set? runtime-cli) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension subids-service-type + oci-service-subids) + (service-extension account-service-type + oci-service-accounts) + (service-extension shepherd-root-service-type + oci-configuration->shepherd-services))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend + (lambda (config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-networks-shepherd-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volumes-shepherd-name))))) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 69ff9f1d9e4..98ee175873d 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -295,17 +300,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..719647c298e 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,952 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %guile-oci-image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + +(define %oci-test-containers + (list + (oci-container-configuration + (provision "first") + (image %guile-oci-image) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image %guile-oci-image) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))) + +(define %oci-extension-test + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers %oci-test-containers))) + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + %oci-extension-test))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + %oci-extension-test))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 05 Feb 2025 22:04:03 GMT) Full text and rfc822 format available.Message #17 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH 2/4] services: oci-container-configuration: Move to (gnu services containers). Date: Wed, 5 Feb 2025 23:02:29 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index dc66ac4f967..50bf6c05549 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -188,7 +237,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -242,3 +291,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 3af0f792701..69ff9f1d9e4 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -307,495 +295,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 19:15:02 GMT) Full text and rfc822 format available.Message #20 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Sun, 9 Feb 2025 20:14:33 +0100
Hi, I'm about to send a v2. v2 compared to the first revision features: - it actually compiles all the times :) (rev 1 referenced oci-image too early for it to be working and generated a compile time error, if you recompiled it sometimes went away so I thought it was a problem of my setup. CI caught this) - it allows more values to be overridden by eventual users of the Scheme API - it allows passing extra arguments directly after each podman or docker invokation, allowing for example for overriding podman --root and similar options. All of these tests should pass: guix shell -D guix -CPW -- make check-system TESTS="oci-container oci-service-rootless-podman docker docker-system rootless-podman oci-service-docker" Thank you for your work, giacomo
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 19:16:01 GMT) Full text and rfc822 format available.Message #23 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v2 1/4] services: rootless-podman: Use login shell. Date: Sun, 9 Feb 2025 20:15:01 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 19d35ccbcb6..dc66ac4f967 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 5a897c5c95a81278b044c18d962d3bd83131ba06 -- 2.48.1
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 19:16:02 GMT) Full text and rfc822 format available.Message #26 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v2 3/4] services: Add oci-service-type. Date: Sun, 9 Feb 2025 20:15:03 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 299 ++++++++-- gnu/services/containers.scm | 1086 +++++++++++++++++++++++++++++++---- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 +++++++++++++++++++++++++++++++- 4 files changed, 2225 insertions(+), 196 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index bb5f29277fb..8f66b35297b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -41778,59 +41778,159 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (default: @code{"oci-container"}) (type: string) +The user name under whose authority OCI commands will be run. + +@item @code{group} (default: @code{"docker"}) (type: string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -41850,16 +41950,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -41867,22 +41967,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -41910,7 +42012,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -41921,10 +42023,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -41935,25 +42038,95 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly +passed to the runtime invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +passed to the runtime invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 50bf6c05549..5c50c99eaf6 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -41,6 +41,7 @@ (define-module (gnu services containers) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +97,72 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-group + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-fields + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-networks-shepherd-name + oci-volumes-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services)) (define (gexp-or-string? value) (or (gexp? value) @@ -294,9 +359,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (maybe-value-set? maybe-group) + maybe-group + (if (eq? 'podman runtime) + "cgroup" + "docker"))) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -345,6 +443,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -372,6 +475,7 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) + "Return a string OCI image reference representing IMAGE." (if (string? image) image (string-append (oci-image-repository image) @@ -390,7 +494,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -436,10 +552,12 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user (string "oci-container") - "The user under whose authority docker commands will be run.") + "The user name under whose authority OCI commands will be run.") (group (string "docker") - "The group under whose authority docker commands will be run.") + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -449,9 +567,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -475,15 +593,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -512,7 +631,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -524,9 +643,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -539,63 +659,326 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invokation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the runtime invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the runtime invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-configuration/no-serialization oci-configuration + (runtime + (symbol 'docker) + "The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}." + (sanitizer oci-sanitize-runtime)) + (runtime-cli + (maybe-package) + "The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime.") + (runtime-extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation.") + (user + (string "oci-container") + "The user name under whose authority OCI runtime commands will be run.") + (group + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") + (subuids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (subgids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (verbose? + (boolean #f) + "When true, additional output will be printed, allowing to better follow the +flow of execution.")) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-system-group runtime user group) + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + group)) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (maybe-value-set? runtime-cli) + runtime-cli + path) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invokation) + "Return a Shepherd action printing a given INVOKATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invokation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invokation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define* (get-keyword-value args keyword #:key (default #f)) (let ((kv (memq keyword args))) @@ -604,6 +987,7 @@ (define* (get-keyword-value args keyword #:key (default #f)) default))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -613,6 +997,7 @@ (define (lower-operating-system os target system) (return tarball))) (define (lower-manifest name image target system) + "Lower IMAGE, a manifest record, into a tarball containing an OCI image." (define value (oci-image-value image)) (define options (oci-image-pack-options image)) (define image-reference @@ -645,6 +1030,7 @@ (define (lower-manifest name image target system) (return tarball))) (define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." (define value (oci-image-value image)) (define image-target (oci-image-target image)) (define image-system (oci-image-system image)) @@ -675,9 +1061,10 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define* (oci-image-loader runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) (program-file (format #f "~a-image-loader" name) #~(begin @@ -686,102 +1073,561 @@ (define (%oci-image-loader name image tag) (ice-9 rdelim)) (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + (when #$verbose? + (format #t "Running ~a~%" load-command)) (define line (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) + (open-input-pipe load-command))) (unless (or (eof-object? line) (string-null? line)) (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag))) + + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) - (invoke #$docker "tag" repository&tag #$tag) + (apply invoke tag-command) (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) +(define (oci-container-run-invokation runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invokation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" + "--rm" "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime-cli name image image-reference + invokation #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%")) + (define invokation (list #$@invokation)) + #$@(if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime-cli name image + image-reference #:verbose? verbose?))) + #~()) + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (apply execlp `(,(first invokation) ,@invokation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (user (or user (oci-container-configuration-user config))) + (group (if (and group (maybe-value-set? group)) + group + (oci-container-configuration-group config))) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invokation + (oci-container-run-invokation + runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group + (list + #$(oci-container-entrypoint + runtime-cli name image image-reference + invokation #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) #$@(if (maybe-value-set? log-file) (list #:log-file log-file) '()) + #$@(if (eq? runtime 'podman) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) #:environment-variables - (list #$@host-environment)))) + (append + (list #$@host-environment) + (list #$@runtime-environment))))) (stop #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (invoke #$runtime-cli "rm" "-f" #$name))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invokation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + #~(lambda _ + (invoke #$runtime-cli "pull" #$image))))))) + actions))))) + +(define (oci-object-create-invokation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invokation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invokations invokations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOKATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invokations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invokations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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 (object-exists? name) + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"{{.Name}}\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command)))))) + + (for-each + (lambda (invokation) + (define name (last invokation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (let ((exit-code (status:exit-val (apply system* invokation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invokations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invokations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOKATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invokations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invokations invokations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invokations + (map + (lambda (network) + (oci-object-create-invokation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invokations + (map + (lambda (volume) + (oci-object-create-invokation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + (let* ((networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol + (oci-networks-shepherd-name runtime))) + '())) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list + (string->symbol + (oci-volumes-shepherd-name runtime))) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime)) + networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime)) + volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (runtime-cli + (oci-runtime-system-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config))) + (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:user user + #:group + (oci-runtime-system-group runtime user group) + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking)))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) (not (string=? (subid-range-name range) user))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subgids) + subgids + (subid-range (name user))) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subuids) + subuids + (subid-range (name user))) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-networks-shepherd-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volumes-shepherd-name)))) + +(define (oci-service-profile runtime runtime-cli) + (list bash-minimal + (cond + ((maybe-value-set? runtime-cli) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension subids-service-type + oci-service-subids) + (service-extension account-service-type + oci-service-accounts) + (service-extension shepherd-root-service-type + oci-configuration->shepherd-services))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend + (lambda (config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-networks-shepherd-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volumes-shepherd-name))))) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 69ff9f1d9e4..98ee175873d 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -295,17 +300,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..bac1f47bd34 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 19:16:02 GMT) Full text and rfc822 format available.Message #29 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v2 4/4] tests: Use lower-oci-image-state in container tests. Date: Sun, 9 Feb 2025 20:15:04 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 35 ++++++++----- gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 114 insertions(+), 101 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 5c50c99eaf6..2808afe7f08 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -74,6 +74,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields @@ -996,12 +998,9 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - "Lower IMAGE, a manifest record, into a tarball containing an OCI image." - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (define image-tag (let* ((extra-options (get-keyword-value options #:extra-options)) @@ -1013,8 +1012,7 @@ (define (lower-manifest name image target system) `(#:extra-options (#:image-tag ,image-reference))))) (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -1029,11 +1027,8 @@ (define (lower-manifest name image target system) #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -1046,7 +1041,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -1061,6 +1057,17 @@ (define (lower-oci-image name image) #:target target #:system system))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + (define* (oci-image-loader runtime-cli name image tag #:key (verbose? #f)) "Return a file-like object that, once lowered, will evaluate to a program able to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index bac1f47bd34..1fcf6be7ace 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 19:16:03 GMT) Full text and rfc822 format available.Message #32 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v2 2/4] services: oci-container-configuration: Move to (gnu services containers). Date: Sun, 9 Feb 2025 20:15:02 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index dc66ac4f967..50bf6c05549 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -188,7 +237,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -242,3 +291,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 3af0f792701..69ff9f1d9e4 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -307,495 +295,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 20:39:02 GMT) Full text and rfc822 format available.Message #35 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Sun, 9 Feb 2025 21:38:47 +0100
I'm sending a v3 fixing a bug in the merge algorithm for volumes and networks. On 2/9/25 20:14, paul wrote: > Hi, > > I'm about to send a v2. v2 compared to the first revision features: > > > - it actually compiles all the times :) (rev 1 referenced oci-image > too early for it to be working and generated a compile time error, if > you recompiled it sometimes went away so I thought it was a problem of > my setup. CI caught this) > - it allows more values to be overridden by eventual users of the > Scheme API > - it allows passing extra arguments directly after each podman or > docker invokation, allowing for example for overriding podman --root > and similar options. > > All of these tests should pass: > > guix shell -D guix -CPW -- make check-system TESTS="oci-container > oci-service-rootless-podman docker docker-system rootless-podman > oci-service-docker" > > > Thank you for your work, > > giacomo >
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 20:40:02 GMT) Full text and rfc822 format available.Message #38 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v3 1/4] services: rootless-podman: Use login shell. Date: Sun, 9 Feb 2025 21:39:05 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 19d35ccbcb6..dc66ac4f967 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 5a897c5c95a81278b044c18d962d3bd83131ba06 -- 2.48.1
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 20:40:03 GMT) Full text and rfc822 format available.Message #41 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v3 3/4] services: Add oci-service-type. Date: Sun, 9 Feb 2025 21:39:07 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 299 ++++++++-- gnu/services/containers.scm | 1086 +++++++++++++++++++++++++++++++---- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 +++++++++++++++++++++++++++++++- 4 files changed, 2225 insertions(+), 196 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index bb5f29277fb..8f66b35297b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -41778,59 +41778,159 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (default: @code{"oci-container"}) (type: string) +The user name under whose authority OCI commands will be run. + +@item @code{group} (default: @code{"docker"}) (type: string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -41850,16 +41950,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -41867,22 +41967,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -41910,7 +42012,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -41921,10 +42023,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -41935,25 +42038,95 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly +passed to the runtime invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +passed to the runtime invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 50bf6c05549..12c509c07ab 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -41,6 +41,7 @@ (define-module (gnu services containers) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +97,72 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-group + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-fields + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-networks-shepherd-name + oci-volumes-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services)) (define (gexp-or-string? value) (or (gexp? value) @@ -294,9 +359,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (maybe-value-set? maybe-group) + maybe-group + (if (eq? 'podman runtime) + "cgroup" + "docker"))) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -345,6 +443,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -372,6 +475,7 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) + "Return a string OCI image reference representing IMAGE." (if (string? image) image (string-append (oci-image-repository image) @@ -390,7 +494,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -436,10 +552,12 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user (string "oci-container") - "The user under whose authority docker commands will be run.") + "The user name under whose authority OCI commands will be run.") (group (string "docker") - "The group under whose authority docker commands will be run.") + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -449,9 +567,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -475,15 +593,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -512,7 +631,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -524,9 +643,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -539,63 +659,326 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invokation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the runtime invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the runtime invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-configuration/no-serialization oci-configuration + (runtime + (symbol 'docker) + "The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}." + (sanitizer oci-sanitize-runtime)) + (runtime-cli + (maybe-package) + "The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime.") + (runtime-extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation.") + (user + (string "oci-container") + "The user name under whose authority OCI runtime commands will be run.") + (group + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") + (subuids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (subgids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (verbose? + (boolean #f) + "When true, additional output will be printed, allowing to better follow the +flow of execution.")) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-system-group runtime user group) + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + group)) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (maybe-value-set? runtime-cli) + runtime-cli + path) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invokation) + "Return a Shepherd action printing a given INVOKATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invokation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invokation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define* (get-keyword-value args keyword #:key (default #f)) (let ((kv (memq keyword args))) @@ -604,6 +987,7 @@ (define* (get-keyword-value args keyword #:key (default #f)) default))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -613,6 +997,7 @@ (define (lower-operating-system os target system) (return tarball))) (define (lower-manifest name image target system) + "Lower IMAGE, a manifest record, into a tarball containing an OCI image." (define value (oci-image-value image)) (define options (oci-image-pack-options image)) (define image-reference @@ -645,6 +1030,7 @@ (define (lower-manifest name image target system) (return tarball))) (define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." (define value (oci-image-value image)) (define image-target (oci-image-target image)) (define image-system (oci-image-system image)) @@ -675,9 +1061,10 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define* (oci-image-loader runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) (program-file (format #f "~a-image-loader" name) #~(begin @@ -686,102 +1073,561 @@ (define (%oci-image-loader name image tag) (ice-9 rdelim)) (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + (when #$verbose? + (format #t "Running ~a~%" load-command)) (define line (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) + (open-input-pipe load-command))) (unless (or (eof-object? line) (string-null? line)) (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag))) + + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) - (invoke #$docker "tag" repository&tag #$tag) + (apply invoke tag-command) (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) +(define (oci-container-run-invokation runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invokation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" + "--rm" "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime-cli name image image-reference + invokation #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%")) + (define invokation (list #$@invokation)) + #$@(if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime-cli name image + image-reference #:verbose? verbose?))) + #~()) + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (apply execlp `(,(first invokation) ,@invokation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (user (or user (oci-container-configuration-user config))) + (group (if (and group (maybe-value-set? group)) + group + (oci-container-configuration-group config))) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invokation + (oci-container-run-invokation + runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group + (list + #$(oci-container-entrypoint + runtime-cli name image image-reference + invokation #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) #$@(if (maybe-value-set? log-file) (list #:log-file log-file) '()) + #$@(if (eq? runtime 'podman) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) #:environment-variables - (list #$@host-environment)))) + (append + (list #$@host-environment) + (list #$@runtime-environment))))) (stop #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (invoke #$runtime-cli "rm" "-f" #$name))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invokation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + #~(lambda _ + (invoke #$runtime-cli "pull" #$image))))))) + actions))))) + +(define (oci-object-create-invokation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invokation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invokations invokations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOKATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invokations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invokations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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 (object-exists? name) + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"{{.Name}}\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command)))))) + + (for-each + (lambda (invokation) + (define name (last invokation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (let ((exit-code (status:exit-val (apply system* invokation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invokations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invokations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOKATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invokations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invokations invokations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invokations + (map + (lambda (network) + (oci-object-create-invokation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invokations + (map + (lambda (volume) + (oci-object-create-invokation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + (let* ((networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol + (oci-networks-shepherd-name runtime))) + '())) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list + (string->symbol + (oci-volumes-shepherd-name runtime))) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime)) + networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime)) + volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (runtime-cli + (oci-runtime-system-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config))) + (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:user user + #:group + (oci-runtime-system-group runtime user group) + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking)))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) (not (string=? (subid-range-name range) user))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subgids) + subgids + (subid-range (name user))) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subuids) + subuids + (subid-range (name user))) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + (list bash-minimal + (cond + ((maybe-value-set? runtime-cli) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension subids-service-type + oci-service-subids) + (service-extension account-service-type + oci-service-accounts) + (service-extension shepherd-root-service-type + oci-configuration->shepherd-services))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend + (lambda (config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name))))) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 69ff9f1d9e4..98ee175873d 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -295,17 +300,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..bac1f47bd34 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 20:40:03 GMT) Full text and rfc822 format available.Message #44 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v3 4/4] tests: Use lower-oci-image-state in container tests. Date: Sun, 9 Feb 2025 21:39:08 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 35 ++++++++----- gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 114 insertions(+), 101 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 12c509c07ab..5979b26e6c9 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -74,6 +74,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields @@ -996,12 +998,9 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - "Lower IMAGE, a manifest record, into a tarball containing an OCI image." - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (define image-tag (let* ((extra-options (get-keyword-value options #:extra-options)) @@ -1013,8 +1012,7 @@ (define (lower-manifest name image target system) `(#:extra-options (#:image-tag ,image-reference))))) (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -1029,11 +1027,8 @@ (define (lower-manifest name image target system) #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -1046,7 +1041,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -1061,6 +1057,17 @@ (define (lower-oci-image name image) #:target target #:system system))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + (define* (oci-image-loader runtime-cli name image tag #:key (verbose? #f)) "Return a file-like object that, once lowered, will evaluate to a program able to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index bac1f47bd34..1fcf6be7ace 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Feb 2025 20:40:04 GMT) Full text and rfc822 format available.Message #47 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v3 2/4] services: oci-container-configuration: Move to (gnu services containers). Date: Sun, 9 Feb 2025 21:39:06 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index dc66ac4f967..50bf6c05549 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -188,7 +237,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -242,3 +291,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 3af0f792701..69ff9f1d9e4 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -307,495 +295,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 12 Feb 2025 01:10:02 GMT) Full text and rfc822 format available.Message #50 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Wed, 12 Feb 2025 02:09:26 +0100
Hi guix, On 2/9/25 21:38, paul wrote: > I'm sending a v3 fixing a bug in the merge algorithm for volumes and > networks. > > On 2/9/25 20:14, paul wrote: >> Hi, >> >> I'm about to send a v2. v2 compared to the first revision features: >> >> >> - it actually compiles all the times :) (rev 1 referenced oci-image >> too early for it to be working and generated a compile time error, if >> you recompiled it sometimes went away so I thought it was a problem >> of my setup. CI caught this) >> - it allows more values to be overridden by eventual users of the >> Scheme API >> - it allows passing extra arguments directly after each podman or >> docker invokation, allowing for example for overriding podman --root >> and similar options. >> >> All of these tests should pass: >> >> guix shell -D guix -CPW -- make check-system TESTS="oci-container >> oci-service-rootless-podman docker docker-system rootless-podman >> oci-service-docker" >> I'm sending a v4 changing slightly the image loader, the same tests as before are supposed to pass. Now the Home service [0] is working for me with rootless podman. I'll try it on different distros if I manage to. Thank you for your work, giacomo [0]: https://github.com/fishinthecalculator/gocix/blob/main/modules/oci/home/services/containers.scm
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 12 Feb 2025 01:11:02 GMT) Full text and rfc822 format available.Message #53 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v4 3/4] services: Add oci-service-type. Date: Wed, 12 Feb 2025 02:10:36 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 305 +++++++-- gnu/services/containers.scm | 1217 ++++++++++++++++++++++++++++++----- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 +++++++++++++++++++++++++++- 4 files changed, 2313 insertions(+), 245 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index ce780682ed0..500cf9f839c 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -41872,59 +41872,161 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -41944,16 +42046,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -41961,22 +42063,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -42004,7 +42108,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -42015,10 +42119,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -42029,25 +42134,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 50bf6c05549..018dfa60f48 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -41,6 +41,7 @@ (define-module (gnu services containers) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +97,73 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-group + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-fields + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-networks-shepherd-name + oci-volumes-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services)) (define (gexp-or-string? value) (or (gexp? value) @@ -294,9 +360,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (maybe-value-set? maybe-group) + maybe-group + (if (eq? 'podman runtime) + "cgroup" + "docker"))) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -345,6 +444,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -372,10 +476,15 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -390,7 +499,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -435,11 +556,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -449,9 +574,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -475,15 +600,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -512,7 +638,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -524,9 +650,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -539,71 +666,331 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invokation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-configuration/no-serialization oci-configuration + (runtime + (symbol 'docker) + "The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}." + (sanitizer oci-sanitize-runtime)) + (runtime-cli + (maybe-package) + "The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime.") + (runtime-extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation.") + (user + (string "oci-container") + "The user name under whose authority OCI runtime commands will be run.") + (group + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") + (subuids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (subgids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (verbose? + (boolean #f) + "When true, additional output will be printed, allowing to better follow the +flow of execution.")) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-system-group runtime user group) + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + group)) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (maybe-value-set? runtime-cli) + runtime-cli + path) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invokation) + "Return a Shepherd action printing a given INVOKATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invokation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invokation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -612,24 +999,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -640,14 +1014,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -660,7 +1031,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -675,113 +1047,622 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define (oci-object-exists? runtime runtime-cli object verbose?) + #~(lambda* (name #:key (format-string "{{.Name}}")) + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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))) + + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"" format-string "\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command))))))) + +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) + (program-file + (format #f "~a-image-loader" name) #~(begin (use-modules (guix build utils) + (ice-9 match) (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) + (ice-9 rdelim) + (srfi srfi-1)) + (define object-exists? + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") + (format #t "~a image already exists, skipping.~%" #$tag) + (begin + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (when #$verbose? + (format #t "Running ~a~%" load-command)) + (let ((line (read-line + (open-input-pipe load-command)))) + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag)) + (drop-old-tag-command + (list #$runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag #$tag) + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) + + (let ((exit-code + (status:exit-val (apply system* tag-command)))) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) + + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + + (when (equal? EXIT_SUCCESS exit-code) + (when #$verbose? + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) + (let ((drop-exit-code + (status:exit-val (apply system* drop-old-tag-command)))) + (when #$verbose? + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) + +(define (oci-container-run-invokation runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invokation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" + "--rm" "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference + invokation #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%")) + (define invokation (list #$@invokation)) + #$@(if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime runtime-cli name image + image-reference #:verbose? verbose?))) + #~()) + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (apply execlp `(,(first invokation) ,@invokation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (maybe-user (oci-container-configuration-user config)) + (maybe-group (oci-container-configuration-group config)) + (user (if (maybe-value-set? maybe-user) + maybe-user + user)) + (group (if (maybe-value-set? maybe-group) + maybe-group + group)) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invokation + (oci-container-run-invokation + runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group + (list + #$(oci-container-entrypoint + runtime runtime-cli name image image-reference + invokation #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) #$@(if (maybe-value-set? log-file) (list #:log-file log-file) '()) + #$@(if (and user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) #:environment-variables - (list #$@host-environment)))) + (append + (list #$@host-environment) + (list #$@runtime-environment))))) (stop #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (invoke #$runtime-cli "rm" "-f" #$name))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invokation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + #~(lambda _ + (invoke #$runtime-cli "pull" #$image))))))) + actions))))) + +(define (oci-object-create-invokation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invokation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invokations invokations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOKATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invokations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invokations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-1)) + + (define object-exists? + #$(oci-object-exists? runtime runtime-cli object verbose?)) + + (for-each + (lambda (invokation) + (define name (last invokation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (let ((exit-code (status:exit-val (apply system* invokation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invokations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invokations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOKATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invokations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invokations invokations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invokations + (map + (lambda (network) + (oci-object-create-invokation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invokations + (map + (lambda (volume) + (oci-object-create-invokation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + (let* ((networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol + (oci-networks-shepherd-name runtime))) + '())) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list + (string->symbol + (oci-volumes-shepherd-name runtime))) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime)) + networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime)) + volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (runtime-cli + (oci-runtime-system-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config))) + (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:user user + #:group + (oci-runtime-system-group runtime user group) + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking)))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subgids) + subgids + (subid-range (name user))) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (maybe-value-set? subuids) + subuids + (subid-range (name user))) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + (list bash-minimal + (cond + ((maybe-value-set? runtime-cli) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension subids-service-type + oci-service-subids) + (service-extension account-service-type + oci-service-accounts) + (service-extension shepherd-root-service-type + oci-configuration->shepherd-services))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend + (lambda (config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name))))) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 69ff9f1d9e4..98ee175873d 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -295,17 +300,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..bac1f47bd34 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 12 Feb 2025 01:11:03 GMT) Full text and rfc822 format available.Message #56 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v4 4/4] tests: Use lower-oci-image-state in container tests. Date: Wed, 12 Feb 2025 02:10:37 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 018dfa60f48..63e9cb37b53 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -74,6 +74,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index bac1f47bd34..1fcf6be7ace 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 12 Feb 2025 01:11:03 GMT) Full text and rfc822 format available.Message #59 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v4 1/4] services: rootless-podman: Use login shell. Date: Wed, 12 Feb 2025 02:10:34 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 19d35ccbcb6..dc66ac4f967 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: f650fc3e15e0ba04b3998daf13db8666fcfd9627 -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 12 Feb 2025 01:11:04 GMT) Full text and rfc822 format available.Message #62 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v4 2/4] services: oci-container-configuration: Move to (gnu services containers). Date: Wed, 12 Feb 2025 02:10:35 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index dc66ac4f967..50bf6c05549 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -188,7 +237,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -242,3 +291,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 3af0f792701..69ff9f1d9e4 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -307,495 +295,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Feb 2025 01:21:02 GMT) Full text and rfc822 format available.Message #65 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Tue, 18 Feb 2025 02:20:12 +0100
Hi, On 2/12/25 02:09, paul wrote: > Hi guix, > > On 2/9/25 21:38, paul wrote: >> I'm sending a v3 fixing a bug in the merge algorithm for volumes and >> networks. >> >> On 2/9/25 20:14, paul wrote: >>> Hi, >>> >>> I'm about to send a v2. v2 compared to the first revision features: >>> >>> >>> - it actually compiles all the times :) (rev 1 referenced oci-image >>> too early for it to be working and generated a compile time error, >>> if you recompiled it sometimes went away so I thought it was a >>> problem of my setup. CI caught this) >>> - it allows more values to be overridden by eventual users of the >>> Scheme API >>> - it allows passing extra arguments directly after each podman or >>> docker invokation, allowing for example for overriding podman --root >>> and similar options. >>> >>> All of these tests should pass: >>> >>> guix shell -D guix -CPW -- make check-system TESTS="oci-container >>> oci-service-rootless-podman docker docker-system rootless-podman >>> oci-service-docker" >>> > I'm sending a v4 changing slightly the image loader, the same tests as > before are supposed to pass. Now the Home service [0] is working for > me with rootless podman. I'll try it on different distros if I manage to. I'm sending a v5 implementing a Home service. The changes compared to v4 are pretty trivial as the plumbing was already there, the only downside is that I'm not able to use for-home? in define-configuration, so I had to reimplement oci-configuration with (guix records) and had to reimplement some validation (gnu services configuration) would figure out magically. Thank you for your work and time, cheers giacomo
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 19 Feb 2025 00:10:02 GMT) Full text and rfc822 format available.Message #68 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v5 3/5] services: Add oci-service-type. Date: Wed, 19 Feb 2025 01:09:20 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 306 +++++++-- gnu/services/containers.scm | 1283 ++++++++++++++++++++++++++++++----- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 ++++++++++++++++++++++++++- 4 files changed, 2380 insertions(+), 245 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 54a736c518c..0dfec66a52b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -42282,59 +42282,162 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package-or-string) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. When a string is passed it will be interpreted as the +absolute file-system path of the selected OCI runtime command. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -42354,16 +42457,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -42371,22 +42474,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -42414,7 +42519,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -42425,10 +42530,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -42439,25 +42545,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 50bf6c05549..bd1b6f14356 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -39,8 +39,10 @@ (define-module (gnu services containers) #:use-module (guix packages) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix records) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +98,80 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-group + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-home-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + oci-configuration-valid? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-container-shepherd-name + oci-networks-shepherd-name + oci-networks-home-shepherd-name + oci-volumes-shepherd-name + oci-volumes-home-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-extension-merge + oci-service-extension-wrap-validate + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services + oci-configuration-extend)) (define (gexp-or-string? value) (or (gexp? value) @@ -294,9 +368,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (eq? maybe-group #f) + (if (eq? 'podman runtime) + "cgroup" + "docker") + maybe-group)) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -345,6 +452,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -372,10 +484,15 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -390,7 +507,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -435,11 +564,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -449,9 +582,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -475,15 +608,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -512,7 +646,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -524,9 +658,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -539,71 +674,363 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invokation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-record-type* <oci-configuration> + oci-configuration + make-oci-configuration + oci-configuration? + this-oci-configuration + + (runtime oci-configuration-runtime + (default 'docker)) + (runtime-cli oci-configuration-runtime-cli + (default #f)) ; package or string + (runtime-extra-arguments oci-configuration-runtime-extra-arguments ; strings or gexps + (default '())) ; or file-like objects + (user oci-configuration-user + (default "oci-container")) + (group oci-configuration-group ; string + (default #f)) + (subuids-range oci-configuration-subuids-range ; subid-range + (default #f)) + (subgids-range oci-configuration-subgids-range ; subid-range + (default #f)) + (containers oci-configuration-containers ; oci-container-configurations + (default '())) + (networks oci-configuration-networks ; oci-network-configurations + (default '())) + (volumes oci-configuration-volumes ; oci-volume-configurations + (default '())) + (verbose? oci-configuration-verbose? + (default #f)) + (home-service? oci-configuration-home-service? + (default for-home?) (innate))) + +(define (package-or-string? value) + (or (package? value) (string? value))) + +(define (oci-configuration-valid? config) + (define runtime-cli + (oci-configuration-runtime-cli config)) + (define group + (oci-configuration-group config)) + (define subuids-range + (oci-configuration-subuids-range config)) + (define subgids-range + (oci-configuration-subgids-range config)) + (and + (symbol? + (oci-sanitize-runtime (oci-configuration-runtime config))) + (or (eq? runtime-cli #f) + (package-or-string? runtime-cli)) + (list? (oci-configuration-runtime-extra-arguments config)) + (string? (oci-configuration-user config)) + (or (eq? group #f) + (string? group)) + (or (eq? subuids-range #f) + (subid-range? subuids-range)) + (or (eq? subgids-range #f) + (subid-range? subgids-range)) + (list-of-oci-containers? + (oci-configuration-containers config)) + (list-of-oci-networks? + (oci-configuration-networks config)) + (list-of-oci-volumes? + (oci-configuration-volumes config)) + (boolean? + (oci-configuration-verbose? config)) + (boolean? + (oci-configuration-home-service? config)))) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-system-group runtime user group) + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + group)) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (eq? runtime-cli #f) + path + runtime-cli) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define (oci-runtime-home-cli config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli + (string-append (getenv "HOME") + "/.guix-home/profile")))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invokation) + "Return a Shepherd action printing a given INVOKATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invokation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invokation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-networks-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-networks-shepherd-name runtime))) + +(define (oci-volumes-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-volumes-shepherd-name runtime))) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -612,24 +1039,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -640,14 +1054,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -660,7 +1071,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -675,113 +1087,648 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define (oci-object-exists? runtime runtime-cli object verbose?) + #~(lambda* (name #:key (format-string "{{.Name}}")) + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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))) + + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"" format-string "\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command))))))) + +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) + (program-file + (format #f "~a-image-loader" name) #~(begin (use-modules (guix build utils) + (ice-9 match) (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) + (ice-9 rdelim) + (srfi srfi-1)) + (define object-exists? + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") + (format #t "~a image already exists, skipping.~%" #$tag) + (begin + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (when #$verbose? + (format #t "Running ~a~%" load-command)) + (let ((line (read-line + (open-input-pipe load-command)))) + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag)) + (drop-old-tag-command + (list #$runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag #$tag) + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) + + (let ((exit-code + (status:exit-val (apply system* tag-command)))) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) + + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + + (when (equal? EXIT_SUCCESS exit-code) + (when #$verbose? + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) + (let ((drop-exit-code + (status:exit-val (apply system* drop-old-tag-command)))) + (when #$verbose? + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) + +(define (oci-container-run-invokation runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invokation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" + "--rm" "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference + invokation #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%")) + (define invokation (list #$@invokation)) + #$@(if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime runtime-cli name image + image-reference #:verbose? verbose?))) + #~()) + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (apply execlp `(,(first invokation) ,@invokation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (maybe-user (oci-container-configuration-user config)) + (maybe-group (oci-container-configuration-group config)) + (user (if (maybe-value-set? maybe-user) + maybe-user + user)) + (group (if (maybe-value-set? maybe-group) + maybe-group + group)) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invokation + (oci-container-run-invokation + runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group + (list + #$(oci-container-entrypoint + runtime runtime-cli name image image-reference + invokation #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) #$@(if (maybe-value-set? log-file) (list #:log-file log-file) '()) + #$@(if (and user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) #:environment-variables - (list #$@host-environment)))) + (append + (list #$@host-environment) + (list #$@runtime-environment))))) (stop #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (invoke #$runtime-cli "rm" "-f" #$name))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invokation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + #~(lambda _ + (invoke #$runtime-cli "pull" #$image))))))) + actions))))) + +(define (oci-object-create-invokation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invokation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invokations invokations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOKATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invokations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invokations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-1)) + + (define object-exists? + #$(oci-object-exists? runtime runtime-cli object verbose?)) + + (for-each + (lambda (invokation) + (define name (last invokation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (let ((exit-code (status:exit-val (apply system* invokation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invokations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invokations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOKATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invokations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invokations invokations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invokations + (map + (lambda (network) + (oci-object-create-invokation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invokations + (map + (lambda (volume) + (oci-object-create-invokation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + (let* ((networks-name + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime))) + (networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol networks-name)) + '())) + (volumes-name + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime))) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list (string->symbol volumes-name)) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + networks-name networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + volumes-name volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (system-runtime-cli + (oci-runtime-system-cli config)) + (home-runtime-cli + (oci-runtime-home-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config)) + (home-service? + (oci-configuration-home-service? config))) + (if home-service? + (oci-state->shepherd-services runtime home-runtime-cli containers networks volumes + #:verbose? verbose? + #:networks-name + (oci-networks-home-shepherd-name runtime) + #:volumes-name + (oci-volumes-home-shepherd-name runtime)) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group + (oci-runtime-system-group runtime user group) + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking))))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subgids #f) + (subid-range (name user)) + subgids) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subuids #f) + (subid-range (name user)) + subuids) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + `(,bash-minimal + ,@(if (string? runtime-cli) + '() + (list + (cond + ((not (eq? runtime-cli #f)) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))))) + +(define (oci-service-extension-wrap-validate extension) + (lambda (config) + (if (oci-configuration-valid? config) + (extension config) + (raise + (formatted-message + (G_ "Invalide oci-configuration ~a.") config))))) + +(define (oci-configuration-extend config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension subids-service-type + (oci-service-extension-wrap-validate + oci-service-subids)) + (service-extension account-service-type + (oci-service-extension-wrap-validate + oci-service-accounts)) + (service-extension shepherd-root-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend oci-configuration-extend) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 69ff9f1d9e4..98ee175873d 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -295,17 +300,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..5e6f39387e7 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 19 Feb 2025 00:10:03 GMT) Full text and rfc822 format available.Message #71 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v5 1/5] services: rootless-podman: Use login shell. Date: Wed, 19 Feb 2025 01:09:18 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 19d35ccbcb6..dc66ac4f967 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 3dc8026d58f9480547a595450a6483e0f13c1ba4 -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 19 Feb 2025 00:10:04 GMT) Full text and rfc822 format available.Message #74 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v5 4/5] tests: Use lower-oci-image-state in container tests. Date: Wed, 19 Feb 2025 01:09:21 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index bd1b6f14356..8b57ac57798 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -75,6 +75,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 5e6f39387e7..8cdd86e7ae3 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
andrew <at> trop.in, janneke <at> gnu.org, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, tanguy <at> bioneland.org, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 19 Feb 2025 00:10:05 GMT) Full text and rfc822 format available.Message #77 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v5 5/5] home: Add home-oci-service-type. Date: Wed, 19 Feb 2025 01:09:22 +0100
* gnu/home/service/containers.scm: New file; * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (OCI backed services): Document it. Change-Id: I8ce5b301e8032d0a7b2a9ca46752738cdee1f030 --- doc/guix.texi | 114 +++++++++++++++++++++++++++++++ gnu/home/services/containers.scm | 50 ++++++++++++++ gnu/local.mk | 1 + 3 files changed, 165 insertions(+) create mode 100644 gnu/home/services/containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index 0dfec66a52b..eacca409473 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -49826,6 +49826,120 @@ Miscellaneous Home Services (dicod-configuration @dots{}))) @end lisp +@subsubheading OCI backed services + +@cindex OCI-backed, for Home +The @code{(gnu home services containers)} module provides the following service: + +@defvar home-oci-service-type +This is the type of the service that allows to manage your OCI containers with +the same consistent interface you use for your other Home Shepherd services. +@end defvar + +This service is a direct mapping of the @code{oci-service-type} system +service (@pxref{Miscellaneous Services, OCI backed services}). You can +use it like this: + +@lisp +(use-modules (gnu services containers) + (gnu home services containers)) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) + +@end lisp + +You may specify a custom configuration by providing a +@code{oci-configuration} record, exactly like for +@code{oci-service-type}, but wrapping it in @code{for-home}: + +@lisp +(use-modules (gnu services) + (gnu services containers) + (gnu home services containers)) + +(service home-oci-service-type + (for-home + (oci-configuration + (runtime 'podman) + (verbose? #t)))) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) +@end lisp + @node Invoking guix home @section Invoking @command{guix home} diff --git a/gnu/home/services/containers.scm b/gnu/home/services/containers.scm new file mode 100644 index 00000000000..938dde2f37a --- /dev/null +++ b/gnu/home/services/containers.scm @@ -0,0 +1,50 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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 home services containers) + #:use-module (gnu home services) + #:use-module (gnu home services shepherd) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services containers) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (home-oci-service-type)) + +(define home-oci-service-type + (service-type (inherit (system->home-service-type oci-service-type)) + (extensions + (list + (service-extension home-profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension home-shepherd-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + (extend + (lambda (config extension) + (for-home + (oci-configuration + (inherit (oci-configuration-extend config extension)))))) + (default-value (for-home (oci-configuration))))) diff --git a/gnu/local.mk b/gnu/local.mk index 01926bb1b8b..37ab59a3ba1 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -102,6 +102,7 @@ GNU_SYSTEM_MODULES = \ %D%/home.scm \ %D%/home/services.scm \ %D%/home/services/admin.scm \ + %D%/home/services/containers.scm \ %D%/home/services/desktop.scm \ %D%/home/services/dict.scm \ %D%/home/services/dotfiles.scm \ -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 19 Feb 2025 00:10:06 GMT) Full text and rfc822 format available.Message #80 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v5 2/5] services: oci-container-configuration: Move to (gnu services containers). Date: Wed, 19 Feb 2025 01:09:19 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index dc66ac4f967..50bf6c05549 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -188,7 +237,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -242,3 +291,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 3af0f792701..69ff9f1d9e4 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -307,495 +295,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 26 Feb 2025 23:02:02 GMT) Full text and rfc822 format available.Message #83 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Thu, 27 Feb 2025 00:01:16 +0100
Hi guix, On 2/18/25 02:20, paul wrote: > Hi, > > On 2/12/25 02:09, paul wrote: >> Hi guix, >> >> On 2/9/25 21:38, paul wrote: >>> I'm sending a v3 fixing a bug in the merge algorithm for volumes and >>> networks. >>> >>> On 2/9/25 20:14, paul wrote: >>>> Hi, >>>> >>>> I'm about to send a v2. v2 compared to the first revision features: >>>> >>>> >>>> - it actually compiles all the times :) (rev 1 referenced oci-image >>>> too early for it to be working and generated a compile time error, >>>> if you recompiled it sometimes went away so I thought it was a >>>> problem of my setup. CI caught this) >>>> - it allows more values to be overridden by eventual users of the >>>> Scheme API >>>> - it allows passing extra arguments directly after each podman or >>>> docker invokation, allowing for example for overriding podman >>>> --root and similar options. >>>> >>>> All of these tests should pass: >>>> >>>> guix shell -D guix -CPW -- make check-system TESTS="oci-container >>>> oci-service-rootless-podman docker docker-system rootless-podman >>>> oci-service-docker" >>>> >> I'm sending a v4 changing slightly the image loader, the same tests >> as before are supposed to pass. Now the Home service [0] is working >> for me with rootless podman. I'll try it on different distros if I >> manage to. > > I'm sending a v5 implementing a Home service. The changes compared to > v4 are pretty trivial as the plumbing was already there, the only > downside is that I'm not able to use for-home? in > define-configuration, so I had to reimplement oci-configuration with > (guix records) and had to reimplement some validation (gnu services > configuration) would figure out magically. I'm sending a v6. Compared to v5 it resolves the conflicts with master and it should fix the stop action for rootless podman backed services. Thank you for all your work, giacomo
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 26 Feb 2025 23:09:01 GMT) Full text and rfc822 format available.Message #86 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v6 1/5] services: rootless-podman: Use login shell. Date: Thu, 27 Feb 2025 00:08:06 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index b3cd109ce6c..d5a211765a6 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 4f220482de742c9c03cf6378ab147026a330edd0 -- 2.48.1
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 26 Feb 2025 23:09:02 GMT) Full text and rfc822 format available.Message #89 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v6 3/5] services: Add oci-service-type. Date: Thu, 27 Feb 2025 00:08:08 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 306 ++++++-- gnu/services/containers.scm | 1314 ++++++++++++++++++++++++++++++----- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 +++++++++++++++++++++++++- 4 files changed, 2404 insertions(+), 252 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index a036c85c31a..fd08261af3b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -42325,59 +42325,162 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package-or-string) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. When a string is passed it will be interpreted as the +absolute file-system path of the selected OCI runtime command. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -42397,16 +42500,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -42414,22 +42517,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -42457,7 +42562,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -42468,10 +42573,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -42482,25 +42588,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 24f31c756b8..603eec25e2b 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -39,8 +39,10 @@ (define-module (gnu services containers) #:use-module (guix packages) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix records) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +98,80 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-group + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-home-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + oci-configuration-valid? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-container-shepherd-name + oci-networks-shepherd-name + oci-networks-home-shepherd-name + oci-volumes-shepherd-name + oci-volumes-home-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-extension-merge + oci-service-extension-wrap-validate + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services + oci-configuration-extend)) (define (gexp-or-string? value) (or (gexp? value) @@ -296,9 +370,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (eq? maybe-group #f) + (if (eq? 'podman runtime) + "cgroup" + "docker") + maybe-group)) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -347,6 +454,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -374,10 +486,15 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -392,7 +509,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -437,11 +566,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -451,9 +584,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -477,15 +610,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -514,7 +648,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -526,9 +660,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -541,71 +676,363 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invocation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-record-type* <oci-configuration> + oci-configuration + make-oci-configuration + oci-configuration? + this-oci-configuration + + (runtime oci-configuration-runtime + (default 'docker)) + (runtime-cli oci-configuration-runtime-cli + (default #f)) ; package or string + (runtime-extra-arguments oci-configuration-runtime-extra-arguments ; strings or gexps + (default '())) ; or file-like objects + (user oci-configuration-user + (default "oci-container")) + (group oci-configuration-group ; string + (default #f)) + (subuids-range oci-configuration-subuids-range ; subid-range + (default #f)) + (subgids-range oci-configuration-subgids-range ; subid-range + (default #f)) + (containers oci-configuration-containers ; oci-container-configurations + (default '())) + (networks oci-configuration-networks ; oci-network-configurations + (default '())) + (volumes oci-configuration-volumes ; oci-volume-configurations + (default '())) + (verbose? oci-configuration-verbose? + (default #f)) + (home-service? oci-configuration-home-service? + (default for-home?) (innate))) + +(define (package-or-string? value) + (or (package? value) (string? value))) + +(define (oci-configuration-valid? config) + (define runtime-cli + (oci-configuration-runtime-cli config)) + (define group + (oci-configuration-group config)) + (define subuids-range + (oci-configuration-subuids-range config)) + (define subgids-range + (oci-configuration-subgids-range config)) + (and + (symbol? + (oci-sanitize-runtime (oci-configuration-runtime config))) + (or (eq? runtime-cli #f) + (package-or-string? runtime-cli)) + (list? (oci-configuration-runtime-extra-arguments config)) + (string? (oci-configuration-user config)) + (or (eq? group #f) + (string? group)) + (or (eq? subuids-range #f) + (subid-range? subuids-range)) + (or (eq? subgids-range #f) + (subid-range? subgids-range)) + (list-of-oci-containers? + (oci-configuration-containers config)) + (list-of-oci-networks? + (oci-configuration-networks config)) + (list-of-oci-volumes? + (oci-configuration-volumes config)) + (boolean? + (oci-configuration-verbose? config)) + (boolean? + (oci-configuration-home-service? config)))) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-system-group runtime user group) + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + group)) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (eq? runtime-cli #f) + path + runtime-cli) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define (oci-runtime-home-cli config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli + (string-append (getenv "HOME") + "/.guix-home/profile")))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invokation) + "Return a Shepherd action printing a given INVOKATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invocation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invokation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-networks-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-networks-shepherd-name runtime))) + +(define (oci-volumes-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-volumes-shepherd-name runtime))) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -614,24 +1041,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -642,14 +1056,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -662,7 +1073,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -677,113 +1089,665 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define (oci-object-exists? runtime runtime-cli object verbose?) + #~(lambda* (name #:key (format-string "{{.Name}}")) + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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))) + + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"" format-string "\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command))))))) + +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) + (program-file + (format #f "~a-image-loader" name) #~(begin (use-modules (guix build utils) + (ice-9 match) (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) + (ice-9 rdelim) + (srfi srfi-1)) + (define object-exists? + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") + (format #t "~a image already exists, skipping.~%" #$tag) + (begin + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (when #$verbose? + (format #t "Running ~a~%" load-command)) + (let ((line (read-line + (open-input-pipe load-command)))) + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag)) + (drop-old-tag-command + (list #$runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag #$tag) + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) + + (let ((exit-code + (status:exit-val (apply system* tag-command)))) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) + + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + + (when (equal? EXIT_SUCCESS exit-code) + (when #$verbose? + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) + (let ((drop-exit-code + (status:exit-val (apply system* drop-old-tag-command)))) + (when #$verbose? + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) + +(define (oci-container-run-invokation runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invocation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" + "--rm" "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference + invokation #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%")) + (define invokation (list #$@invokation)) + #$@(if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime runtime-cli name image + image-reference #:verbose? verbose?))) + #~()) + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (apply execlp `(,(first invokation) ,@invokation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (maybe-user (oci-container-configuration-user config)) + (maybe-group (oci-container-configuration-group config)) + (user (if (maybe-value-set? maybe-user) + maybe-user + user)) + (group (if (maybe-value-set? maybe-group) + maybe-group + group)) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invokation + (oci-container-run-invokation + runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments)) + (wrap-command + (lambda (command) + (if (eq? runtime 'podman) + (list + "/bin/sh" "-l" "-c" + #~(string-join (list #$@command) " ")) + command))) + (container-action + (lambda* (command #:key (environment-variables #f)) + #~(lambda _ + (fork+exec-command + (list #$@command) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #$@(if (and user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) + #$@(if environment-variables + (list #:environment-variables + environment-variables) + '())))))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) + (container-action + (list (oci-container-entrypoint + runtime runtime-cli name image image-reference + invokation #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (container-action + (wrap-command + (list runtime-cli "rm" "-f" name)))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invokation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + (container-action + (wrap-command + (list runtime-cli "pull" image)))))))) + actions))))) + +(define (oci-object-create-invokation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invocation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invokations invokations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOKATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invokations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invokations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-1)) + + (define object-exists? + #$(oci-object-exists? runtime runtime-cli object verbose?)) + + (for-each + (lambda (invokation) + (define name (last invokation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invokation)) + (let ((exit-code (status:exit-val (apply system* invokation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invokations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invokations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOKATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invokations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invokations invokations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invokations + (map + (lambda (network) + (oci-object-create-invokation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invokations + (map + (lambda (volume) + (oci-object-create-invokation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invokations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + "Returns a list of Shepherd services based on the input OCI state." + (let* ((networks-name + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime))) + (networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol networks-name)) + '())) + (volumes-name + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime))) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list (string->symbol volumes-name)) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + networks-name networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + volumes-name volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (system-runtime-cli + (oci-runtime-system-cli config)) + (home-runtime-cli + (oci-runtime-home-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config)) + (home-service? + (oci-configuration-home-service? config))) + (if home-service? + (oci-state->shepherd-services runtime home-runtime-cli containers networks volumes + #:verbose? verbose? + #:networks-name + (oci-networks-home-shepherd-name runtime) + #:volumes-name + (oci-volumes-home-shepherd-name runtime)) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group + (oci-runtime-system-group runtime user group) + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking))))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subgids #f) + (subid-range (name user)) + subgids) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subuids #f) + (subid-range (name user)) + subuids) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + `(,bash-minimal + ,@(if (string? runtime-cli) + '() + (list + (cond + ((not (eq? runtime-cli #f)) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))))) + +(define (oci-service-extension-wrap-validate extension) + (lambda (config) + (if (oci-configuration-valid? config) + (extension config) + (raise + (formatted-message + (G_ "Invalide oci-configuration ~a.") config))))) + +(define (oci-configuration-extend config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension subids-service-type + (oci-service-extension-wrap-validate + oci-service-subids)) + (service-extension account-service-type + (oci-service-extension-wrap-validate + oci-service-accounts)) + (service-extension shepherd-root-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend oci-configuration-extend) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 828ceea313a..125e748bb0e 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -297,17 +302,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..5e6f39387e7 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 26 Feb 2025 23:09:02 GMT) Full text and rfc822 format available.Message #92 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v6 4/5] tests: Use lower-oci-image-state in container tests. Date: Thu, 27 Feb 2025 00:08:09 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 603eec25e2b..139d5e4ec50 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -75,6 +75,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 5e6f39387e7..8cdd86e7ae3 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
andrew <at> trop.in, janneke <at> gnu.org, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, tanguy <at> bioneland.org, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 26 Feb 2025 23:09:03 GMT) Full text and rfc822 format available.Message #95 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v6 5/5] home: Add home-oci-service-type. Date: Thu, 27 Feb 2025 00:08:10 +0100
* gnu/home/service/containers.scm: New file; * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (OCI backed services): Document it. Change-Id: I8ce5b301e8032d0a7b2a9ca46752738cdee1f030 --- doc/guix.texi | 114 +++++++++++++++++++++++++++++++ gnu/home/services/containers.scm | 50 ++++++++++++++ gnu/local.mk | 1 + 3 files changed, 165 insertions(+) create mode 100644 gnu/home/services/containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index fd08261af3b..5513eb89048 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -49876,6 +49876,120 @@ Miscellaneous Home Services (dicod-configuration @dots{}))) @end lisp +@subsubheading OCI backed services + +@cindex OCI-backed, for Home +The @code{(gnu home services containers)} module provides the following service: + +@defvar home-oci-service-type +This is the type of the service that allows to manage your OCI containers with +the same consistent interface you use for your other Home Shepherd services. +@end defvar + +This service is a direct mapping of the @code{oci-service-type} system +service (@pxref{Miscellaneous Services, OCI backed services}). You can +use it like this: + +@lisp +(use-modules (gnu services containers) + (gnu home services containers)) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) + +@end lisp + +You may specify a custom configuration by providing a +@code{oci-configuration} record, exactly like for +@code{oci-service-type}, but wrapping it in @code{for-home}: + +@lisp +(use-modules (gnu services) + (gnu services containers) + (gnu home services containers)) + +(service home-oci-service-type + (for-home + (oci-configuration + (runtime 'podman) + (verbose? #t)))) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) +@end lisp + @node Invoking guix home @section Invoking @command{guix home} diff --git a/gnu/home/services/containers.scm b/gnu/home/services/containers.scm new file mode 100644 index 00000000000..938dde2f37a --- /dev/null +++ b/gnu/home/services/containers.scm @@ -0,0 +1,50 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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 home services containers) + #:use-module (gnu home services) + #:use-module (gnu home services shepherd) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services containers) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (home-oci-service-type)) + +(define home-oci-service-type + (service-type (inherit (system->home-service-type oci-service-type)) + (extensions + (list + (service-extension home-profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension home-shepherd-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + (extend + (lambda (config extension) + (for-home + (oci-configuration + (inherit (oci-configuration-extend config extension)))))) + (default-value (for-home (oci-configuration))))) diff --git a/gnu/local.mk b/gnu/local.mk index c8a29bf98b5..9b86e59c37f 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -102,6 +102,7 @@ GNU_SYSTEM_MODULES = \ %D%/home.scm \ %D%/home/services.scm \ %D%/home/services/admin.scm \ + %D%/home/services/containers.scm \ %D%/home/services/desktop.scm \ %D%/home/services/dict.scm \ %D%/home/services/dotfiles.scm \ -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Wed, 26 Feb 2025 23:09:03 GMT) Full text and rfc822 format available.Message #98 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v6 2/5] services: oci-container-configuration: Move to (gnu services containers). Date: Thu, 27 Feb 2025 00:08:07 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index d5a211765a6..24f31c756b8 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -190,7 +239,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -244,3 +293,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 9ab3e583345..828ceea313a 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -309,495 +297,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invocation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 04 Mar 2025 12:40:02 GMT) Full text and rfc822 format available.Message #101 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Tue, 4 Mar 2025 13:39:33 +0100
Hi, On 2/27/25 00:01, paul wrote: > Hi guix, > > On 2/18/25 02:20, paul wrote: >> Hi, >> >> On 2/12/25 02:09, paul wrote: >>> Hi guix, >>> >>> On 2/9/25 21:38, paul wrote: >>>> I'm sending a v3 fixing a bug in the merge algorithm for volumes >>>> and networks. >>>> >>>> On 2/9/25 20:14, paul wrote: >>>>> Hi, >>>>> >>>>> I'm about to send a v2. v2 compared to the first revision features: >>>>> >>>>> >>>>> - it actually compiles all the times :) (rev 1 referenced >>>>> oci-image too early for it to be working and generated a compile >>>>> time error, if you recompiled it sometimes went away so I thought >>>>> it was a problem of my setup. CI caught this) >>>>> - it allows more values to be overridden by eventual users of the >>>>> Scheme API >>>>> - it allows passing extra arguments directly after each podman or >>>>> docker invokation, allowing for example for overriding podman >>>>> --root and similar options. >>>>> >>>>> All of these tests should pass: >>>>> >>>>> guix shell -D guix -CPW -- make check-system TESTS="oci-container >>>>> oci-service-rootless-podman docker docker-system rootless-podman >>>>> oci-service-docker" >>>>> >>> I'm sending a v4 changing slightly the image loader, the same tests >>> as before are supposed to pass. Now the Home service [0] is working >>> for me with rootless podman. I'll try it on different distros if I >>> manage to. >> >> I'm sending a v5 implementing a Home service. The changes compared to >> v4 are pretty trivial as the plumbing was already there, the only >> downside is that I'm not able to use for-home? in >> define-configuration, so I had to reimplement oci-configuration with >> (guix records) and had to reimplement some validation (gnu services >> configuration) would figure out magically. > > I'm sending a v6. Compared to v5 it resolves the conflicts with > master and it should fix the stop action for rootless podman backed > services. I'm sending a v7 fixing the restart action for podman backed services. Thank you for your work, giacomo
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 04 Mar 2025 12:42:01 GMT) Full text and rfc822 format available.Message #104 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v7 3/5] services: Add oci-service-type. Date: Tue, 4 Mar 2025 13:40:00 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 306 ++++++-- gnu/services/containers.scm | 1320 ++++++++++++++++++++++++++++++----- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 +++++++++++++++++++++++++- 4 files changed, 2410 insertions(+), 252 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 551bc52f7f6..be5c8cf8fad 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -42727,59 +42727,162 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package-or-string) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. When a string is passed it will be interpreted as the +absolute file-system path of the selected OCI runtime command. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -42799,16 +42902,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -42816,22 +42919,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -42859,7 +42964,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -42870,10 +42975,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -42884,25 +42990,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 24f31c756b8..4600846ac3d 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -39,8 +39,10 @@ (define-module (gnu services containers) #:use-module (guix packages) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix records) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +98,80 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-group + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-home-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + oci-configuration-valid? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-container-shepherd-name + oci-networks-shepherd-name + oci-networks-home-shepherd-name + oci-volumes-shepherd-name + oci-volumes-home-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-extension-merge + oci-service-extension-wrap-validate + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services + oci-configuration-extend)) (define (gexp-or-string? value) (or (gexp? value) @@ -296,9 +370,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (eq? maybe-group #f) + (if (eq? 'podman runtime) + "cgroup" + "docker") + maybe-group)) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -347,6 +454,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -374,10 +486,15 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -392,7 +509,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -437,11 +566,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -451,9 +584,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -477,15 +610,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -514,7 +648,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -526,9 +660,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -541,71 +676,363 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invocation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-record-type* <oci-configuration> + oci-configuration + make-oci-configuration + oci-configuration? + this-oci-configuration + + (runtime oci-configuration-runtime + (default 'docker)) + (runtime-cli oci-configuration-runtime-cli + (default #f)) ; package or string + (runtime-extra-arguments oci-configuration-runtime-extra-arguments ; strings or gexps + (default '())) ; or file-like objects + (user oci-configuration-user + (default "oci-container")) + (group oci-configuration-group ; string + (default #f)) + (subuids-range oci-configuration-subuids-range ; subid-range + (default #f)) + (subgids-range oci-configuration-subgids-range ; subid-range + (default #f)) + (containers oci-configuration-containers ; oci-container-configurations + (default '())) + (networks oci-configuration-networks ; oci-network-configurations + (default '())) + (volumes oci-configuration-volumes ; oci-volume-configurations + (default '())) + (verbose? oci-configuration-verbose? + (default #f)) + (home-service? oci-configuration-home-service? + (default for-home?) (innate))) + +(define (package-or-string? value) + (or (package? value) (string? value))) + +(define (oci-configuration-valid? config) + (define runtime-cli + (oci-configuration-runtime-cli config)) + (define group + (oci-configuration-group config)) + (define subuids-range + (oci-configuration-subuids-range config)) + (define subgids-range + (oci-configuration-subgids-range config)) + (and + (symbol? + (oci-sanitize-runtime (oci-configuration-runtime config))) + (or (eq? runtime-cli #f) + (package-or-string? runtime-cli)) + (list? (oci-configuration-runtime-extra-arguments config)) + (string? (oci-configuration-user config)) + (or (eq? group #f) + (string? group)) + (or (eq? subuids-range #f) + (subid-range? subuids-range)) + (or (eq? subgids-range #f) + (subid-range? subgids-range)) + (list-of-oci-containers? + (oci-configuration-containers config)) + (list-of-oci-networks? + (oci-configuration-networks config)) + (list-of-oci-volumes? + (oci-configuration-volumes config)) + (boolean? + (oci-configuration-verbose? config)) + (boolean? + (oci-configuration-home-service? config)))) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-system-group runtime user group) + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + group)) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (eq? runtime-cli #f) + path + runtime-cli) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define (oci-runtime-home-cli config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli + (string-append (getenv "HOME") + "/.guix-home/profile")))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invocation) + "Return a Shepherd action printing a given INVOCATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invocation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invocation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-networks-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-networks-shepherd-name runtime))) + +(define (oci-volumes-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-volumes-shepherd-name runtime))) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -614,24 +1041,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -642,14 +1056,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -662,7 +1073,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -677,113 +1089,671 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define (oci-object-exists? runtime runtime-cli object verbose?) + #~(lambda* (name #:key (format-string "{{.Name}}")) + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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))) + + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"" format-string "\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command))))))) + +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) + (program-file + (format #f "~a-image-loader" name) #~(begin (use-modules (guix build utils) + (ice-9 match) (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) + (ice-9 rdelim) + (srfi srfi-1)) + (define object-exists? + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") + (format #t "~a image already exists, skipping.~%" #$tag) + (begin + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (when #$verbose? + (format #t "Running ~a~%" load-command)) + (let ((line (read-line + (open-input-pipe load-command)))) + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag)) + (drop-old-tag-command + (list #$runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag #$tag) + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) + + (let ((exit-code + (status:exit-val (apply system* tag-command)))) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) + + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + + (when (equal? EXIT_SUCCESS exit-code) + (when #$verbose? + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) + (let ((drop-exit-code + (status:exit-val (apply system* drop-old-tag-command)))) + (when #$verbose? + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) + +(define (oci-container-run-invocation runtime runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invocation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" "--rm" + ,@(if (eq? runtime 'podman) + ;; This is because podman takes some time to + ;; release container names. --replace seems + ;; to be required to be able to restart services. + '("--replace") + '()) + "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference + invocation #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%")) + (define invocation (list #$@invocation)) + #$@(if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime runtime-cli name image + image-reference #:verbose? verbose?))) + #~()) + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (apply execlp `(,(first invocation) ,@invocation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (maybe-user (oci-container-configuration-user config)) + (maybe-group (oci-container-configuration-group config)) + (user (if (maybe-value-set? maybe-user) + maybe-user + user)) + (group (if (maybe-value-set? maybe-group) + maybe-group + group)) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invocation + (oci-container-run-invocation + runtime runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments)) + (wrap-command + (lambda (command) + (if (eq? runtime 'podman) + (list + "/bin/sh" "-l" "-c" + #~(string-join (list #$@command) " ")) + command))) + (container-action + (lambda* (command #:key (environment-variables #f)) + #~(lambda _ + (fork+exec-command + (list #$@command) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #$@(if (and user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) + #$@(if environment-variables + (list #:environment-variables + environment-variables) + '())))))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) + (container-action + (list (oci-container-entrypoint + runtime runtime-cli name image image-reference + invocation #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (container-action + (wrap-command + (list runtime-cli "rm" "-f" name)))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invocation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + (container-action + (wrap-command + (list runtime-cli "pull" image)))))))) + actions))))) + +(define (oci-object-create-invocation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invocation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invocations invocations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOCATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invocations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invocations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-1)) + + (define object-exists? + #$(oci-object-exists? runtime runtime-cli object verbose?)) + + (for-each + (lambda (invocation) + (define name (last invocation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (let ((exit-code (status:exit-val (apply system* invocation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invocations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invocations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOCATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invocations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invocations invocations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invocations + (map + (lambda (network) + (oci-object-create-invocation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invocations + (map + (lambda (volume) + (oci-object-create-invocation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + "Returns a list of Shepherd services based on the input OCI state." + (let* ((networks-name + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime))) + (networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol networks-name)) + '())) + (volumes-name + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime))) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list (string->symbol volumes-name)) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + networks-name networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + volumes-name volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (system-runtime-cli + (oci-runtime-system-cli config)) + (home-runtime-cli + (oci-runtime-home-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config)) + (home-service? + (oci-configuration-home-service? config))) + (if home-service? + (oci-state->shepherd-services runtime home-runtime-cli containers networks volumes + #:verbose? verbose? + #:networks-name + (oci-networks-home-shepherd-name runtime) + #:volumes-name + (oci-volumes-home-shepherd-name runtime)) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group + (oci-runtime-system-group runtime user group) + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking))))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subgids #f) + (subid-range (name user)) + subgids) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subuids #f) + (subid-range (name user)) + subuids) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + `(,bash-minimal + ,@(if (string? runtime-cli) + '() + (list + (cond + ((not (eq? runtime-cli #f)) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))))) + +(define (oci-service-extension-wrap-validate extension) + (lambda (config) + (if (oci-configuration-valid? config) + (extension config) + (raise + (formatted-message + (G_ "Invalide oci-configuration ~a.") config))))) + +(define (oci-configuration-extend config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension subids-service-type + (oci-service-extension-wrap-validate + oci-service-subids)) + (service-extension account-service-type + (oci-service-extension-wrap-validate + oci-service-accounts)) + (service-extension shepherd-root-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend oci-configuration-extend) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 828ceea313a..125e748bb0e 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -297,17 +302,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..5e6f39387e7 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 04 Mar 2025 12:42:02 GMT) Full text and rfc822 format available.Message #107 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v7 4/5] tests: Use lower-oci-image-state in container tests. Date: Tue, 4 Mar 2025 13:40:01 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 4600846ac3d..57b14868f1a 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -75,6 +75,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 5e6f39387e7..8cdd86e7ae3 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 04 Mar 2025 12:42:02 GMT) Full text and rfc822 format available.Message #110 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v7 1/5] services: rootless-podman: Use login shell. Date: Tue, 4 Mar 2025 13:39:58 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index b3cd109ce6c..d5a211765a6 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 72923a75af53a819f2be9dc4ae3c096aa3147d3f -- 2.48.1
andrew <at> trop.in, janneke <at> gnu.org, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, tanguy <at> bioneland.org, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 04 Mar 2025 12:42:03 GMT) Full text and rfc822 format available.Message #113 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v7 5/5] home: Add home-oci-service-type. Date: Tue, 4 Mar 2025 13:40:02 +0100
* gnu/home/service/containers.scm: New file; * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (OCI backed services): Document it. Change-Id: I8ce5b301e8032d0a7b2a9ca46752738cdee1f030 --- doc/guix.texi | 114 +++++++++++++++++++++++++++++++ gnu/home/services/containers.scm | 50 ++++++++++++++ gnu/local.mk | 1 + 3 files changed, 165 insertions(+) create mode 100644 gnu/home/services/containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index be5c8cf8fad..6c09f929049 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -50288,6 +50288,120 @@ Miscellaneous Home Services (dicod-configuration @dots{}))) @end lisp +@subsubheading OCI backed services + +@cindex OCI-backed, for Home +The @code{(gnu home services containers)} module provides the following service: + +@defvar home-oci-service-type +This is the type of the service that allows to manage your OCI containers with +the same consistent interface you use for your other Home Shepherd services. +@end defvar + +This service is a direct mapping of the @code{oci-service-type} system +service (@pxref{Miscellaneous Services, OCI backed services}). You can +use it like this: + +@lisp +(use-modules (gnu services containers) + (gnu home services containers)) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) + +@end lisp + +You may specify a custom configuration by providing a +@code{oci-configuration} record, exactly like for +@code{oci-service-type}, but wrapping it in @code{for-home}: + +@lisp +(use-modules (gnu services) + (gnu services containers) + (gnu home services containers)) + +(service home-oci-service-type + (for-home + (oci-configuration + (runtime 'podman) + (verbose? #t)))) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) +@end lisp + @node Invoking guix home @section Invoking @command{guix home} diff --git a/gnu/home/services/containers.scm b/gnu/home/services/containers.scm new file mode 100644 index 00000000000..938dde2f37a --- /dev/null +++ b/gnu/home/services/containers.scm @@ -0,0 +1,50 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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 home services containers) + #:use-module (gnu home services) + #:use-module (gnu home services shepherd) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services containers) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (home-oci-service-type)) + +(define home-oci-service-type + (service-type (inherit (system->home-service-type oci-service-type)) + (extensions + (list + (service-extension home-profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension home-shepherd-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + (extend + (lambda (config extension) + (for-home + (oci-configuration + (inherit (oci-configuration-extend config extension)))))) + (default-value (for-home (oci-configuration))))) diff --git a/gnu/local.mk b/gnu/local.mk index a7a3238669d..d48e767154c 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -103,6 +103,7 @@ GNU_SYSTEM_MODULES = \ %D%/home.scm \ %D%/home/services.scm \ %D%/home/services/admin.scm \ + %D%/home/services/containers.scm \ %D%/home/services/desktop.scm \ %D%/home/services/dict.scm \ %D%/home/services/dotfiles.scm \ -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 04 Mar 2025 12:42:03 GMT) Full text and rfc822 format available.Message #116 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v7 2/5] services: oci-container-configuration: Move to (gnu services containers). Date: Tue, 4 Mar 2025 13:39:59 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index d5a211765a6..24f31c756b8 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -190,7 +239,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -244,3 +293,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 9ab3e583345..828ceea313a 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -309,495 +297,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invocation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Mar 2025 00:28:02 GMT) Full text and rfc822 format available.Message #119 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Sun, 9 Mar 2025 01:27:48 +0100
Hi, On 3/4/25 13:39, paul wrote: > Hi, > > On 2/27/25 00:01, paul wrote: >> Hi guix, >> >> On 2/18/25 02:20, paul wrote: >>> Hi, >>> >>> On 2/12/25 02:09, paul wrote: >>>> Hi guix, >>>> >>>> On 2/9/25 21:38, paul wrote: >>>>> I'm sending a v3 fixing a bug in the merge algorithm for volumes >>>>> and networks. >>>>> >>>>> On 2/9/25 20:14, paul wrote: >>>>>> Hi, >>>>>> >>>>>> I'm about to send a v2. v2 compared to the first revision features: >>>>>> >>>>>> >>>>>> - it actually compiles all the times :) (rev 1 referenced >>>>>> oci-image too early for it to be working and generated a compile >>>>>> time error, if you recompiled it sometimes went away so I thought >>>>>> it was a problem of my setup. CI caught this) >>>>>> - it allows more values to be overridden by eventual users of the >>>>>> Scheme API >>>>>> - it allows passing extra arguments directly after each podman or >>>>>> docker invokation, allowing for example for overriding podman >>>>>> --root and similar options. >>>>>> >>>>>> All of these tests should pass: >>>>>> >>>>>> guix shell -D guix -CPW -- make check-system TESTS="oci-container >>>>>> oci-service-rootless-podman docker docker-system rootless-podman >>>>>> oci-service-docker" >>>>>> >>>> I'm sending a v4 changing slightly the image loader, the same tests >>>> as before are supposed to pass. Now the Home service [0] is working >>>> for me with rootless podman. I'll try it on different distros if I >>>> manage to. >>> >>> I'm sending a v5 implementing a Home service. The changes compared >>> to v4 are pretty trivial as the plumbing was already there, the only >>> downside is that I'm not able to use for-home? in >>> define-configuration, so I had to reimplement oci-configuration with >>> (guix records) and had to reimplement some validation (gnu services >>> configuration) would figure out magically. >> >> I'm sending a v6. Compared to v5 it resolves the conflicts with >> master and it should fix the stop action for rootless podman backed >> services. > > I'm sending a v7 fixing the restart action for podman backed services. I'm sending a v8 fixing some further bugs wrt to the start and stop of podman backed services. Thank you for your work, cheers giacomo
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Mar 2025 01:07:02 GMT) Full text and rfc822 format available.Message #122 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v8 3/5] services: Add oci-service-type. Date: Sun, 9 Mar 2025 02:06:13 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 306 ++++++-- gnu/services/containers.scm | 1335 ++++++++++++++++++++++++++++++----- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 +++++++++++++++++++++++++- 4 files changed, 2425 insertions(+), 252 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 7d8a5243ed8..8686380669b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -42839,59 +42839,162 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package-or-string) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources. When unset it will default to @code{docker-cli} +package for the @code{'docker} runtime or to @code{podman} package for the +@code{'podman} runtime. When a string is passed it will be interpreted as the +absolute file-system path of the selected OCI runtime command. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -42911,16 +43014,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -42928,22 +43031,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -42971,7 +43076,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -42982,10 +43087,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -42996,25 +43102,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 24f31c756b8..a78be00f038 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -39,8 +39,10 @@ (define-module (gnu services containers) #:use-module (guix packages) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix records) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +98,80 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-group + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-home-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + oci-configuration-valid? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-container-shepherd-name + oci-networks-shepherd-name + oci-networks-home-shepherd-name + oci-volumes-shepherd-name + oci-volumes-home-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-extension-merge + oci-service-extension-wrap-validate + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services + oci-configuration-extend)) (define (gexp-or-string? value) (or (gexp? value) @@ -296,9 +370,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (eq? maybe-group #f) + (if (eq? 'podman runtime) + "cgroup" + "docker") + maybe-group)) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -347,6 +454,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -374,10 +486,15 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -392,7 +509,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -437,11 +566,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -451,9 +584,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -477,15 +610,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -514,7 +648,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -526,9 +660,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -541,71 +676,363 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invocation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define-record-type* <oci-configuration> + oci-configuration + make-oci-configuration + oci-configuration? + this-oci-configuration + + (runtime oci-configuration-runtime + (default 'docker)) + (runtime-cli oci-configuration-runtime-cli + (default #f)) ; package or string + (runtime-extra-arguments oci-configuration-runtime-extra-arguments ; strings or gexps + (default '())) ; or file-like objects + (user oci-configuration-user + (default "oci-container")) + (group oci-configuration-group ; string + (default #f)) + (subuids-range oci-configuration-subuids-range ; subid-range + (default #f)) + (subgids-range oci-configuration-subgids-range ; subid-range + (default #f)) + (containers oci-configuration-containers ; oci-container-configurations + (default '())) + (networks oci-configuration-networks ; oci-network-configurations + (default '())) + (volumes oci-configuration-volumes ; oci-volume-configurations + (default '())) + (verbose? oci-configuration-verbose? + (default #f)) + (home-service? oci-configuration-home-service? + (default for-home?) (innate))) + +(define (package-or-string? value) + (or (package? value) (string? value))) + +(define (oci-configuration-valid? config) + (define runtime-cli + (oci-configuration-runtime-cli config)) + (define group + (oci-configuration-group config)) + (define subuids-range + (oci-configuration-subuids-range config)) + (define subgids-range + (oci-configuration-subgids-range config)) + (and + (symbol? + (oci-sanitize-runtime (oci-configuration-runtime config))) + (or (eq? runtime-cli #f) + (package-or-string? runtime-cli)) + (list? (oci-configuration-runtime-extra-arguments config)) + (string? (oci-configuration-user config)) + (or (eq? group #f) + (string? group)) + (or (eq? subuids-range #f) + (subid-range? subuids-range)) + (or (eq? subgids-range #f) + (subid-range? subgids-range)) + (list-of-oci-containers? + (oci-configuration-containers config)) + (list-of-oci-networks? + (oci-configuration-networks config)) + (list-of-oci-volumes? + (oci-configuration-volumes config)) + (boolean? + (oci-configuration-verbose? config)) + (boolean? + (oci-configuration-home-service? config)))) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-system-group runtime user group) + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + group)) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (eq? runtime-cli #f) + path + runtime-cli) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define (oci-runtime-home-cli config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli + (string-append (getenv "HOME") + "/.guix-home/profile")))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invocation) + "Return a Shepherd action printing a given INVOCATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invocation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invocation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-networks-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-networks-shepherd-name runtime))) + +(define (oci-volumes-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-volumes-shepherd-name runtime))) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -614,24 +1041,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -642,14 +1056,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -662,7 +1073,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -677,113 +1089,686 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define (oci-object-exists? runtime runtime-cli object verbose?) + #~(lambda* (name #:key (format-string "{{.Name}}")) + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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))) + + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"" format-string "\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command))))))) + +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) + (program-file + (format #f "~a-image-loader" name) #~(begin (use-modules (guix build utils) + (ice-9 match) (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) + (ice-9 rdelim) + (srfi srfi-1)) + (define object-exists? + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") + (format #t "~a image already exists, skipping.~%" #$tag) + (begin + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (when #$verbose? + (format #t "Running ~a~%" load-command)) + (let ((line (read-line + (open-input-pipe load-command)))) + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag)) + (drop-old-tag-command + (list #$runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag #$tag) + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) + + (let ((exit-code + (status:exit-val (apply system* tag-command)))) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) + + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + + (when (equal? EXIT_SUCCESS exit-code) + (when #$verbose? + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) + (let ((drop-exit-code + (status:exit-val (apply system* drop-old-tag-command)))) + (when #$verbose? + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) + +(define (oci-container-run-invocation runtime runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invocation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" "--rm" + ,@(if (eq? runtime 'podman) + ;; This is because podman takes some time to + ;; release container names. --replace seems + ;; to be required to be able to restart services. + '("--replace") + '()) + "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference + invocation #:key (verbose? #f) (pre-script #~())) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%") + (format #t "Current user: ~a ~a~%" + (getuid) (passwd:name (getpwuid (getuid)))) + (format #t "Current group: ~a ~a~%" + (getgid) (group:name (getgrgid (getgid)))) + (format #t "Current directory ~a~%" (getcwd))) + (define invocation (list #$@invocation)) + #$@pre-script + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (apply execlp `(,(first invocation) ,@invocation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (maybe-user (oci-container-configuration-user config)) + (maybe-group (oci-container-configuration-group config)) + (user (if (maybe-value-set? maybe-user) + maybe-user + user)) + (group (if (maybe-value-set? maybe-group) + maybe-group + group)) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invocation + (oci-container-run-invocation + runtime runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments)) + (container-action + (lambda* (command #:key (environment-variables #f)) + #~(lambda _ + (fork+exec-command + (list #$@command) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #$@(if (and user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) + #$@(if environment-variables + (list #:environment-variables + environment-variables) + '())))))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) + (container-action + (list (oci-container-entrypoint + runtime runtime-cli name image image-reference + invocation #:verbose? verbose? + #:pre-script + (if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime runtime-cli name image + image-reference #:verbose? verbose?))) + #~()))) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (container-action + (list + (oci-container-entrypoint + runtime runtime-cli name image image-reference + (list runtime-cli "rm" "-f" name) + #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invocation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + (container-action + (list + (oci-container-entrypoint + runtime runtime-cli service-name image + image-reference + (list runtime-cli "pull" image) + #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))))))) + actions))))) + +(define (oci-object-create-invocation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invocation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invocations invocations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOCATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invocations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invocations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-1)) + + (define object-exists? + #$(oci-object-exists? runtime runtime-cli object verbose?)) + + (for-each + (lambda (invocation) + (define name (last invocation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (let ((exit-code (status:exit-val (apply system* invocation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invocations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invocations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOCATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invocations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invocations invocations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invocations + (map + (lambda (network) + (oci-object-create-invocation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invocations + (map + (lambda (volume) + (oci-object-create-invocation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + "Returns a list of Shepherd services based on the input OCI state." + (let* ((networks-name + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime))) + (networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol networks-name)) + '())) + (volumes-name + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime))) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list (string->symbol volumes-name)) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + networks-name networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + volumes-name volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (system-runtime-cli + (oci-runtime-system-cli config)) + (home-runtime-cli + (oci-runtime-home-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group (oci-runtime-group + runtime (oci-configuration-group config))) + (verbose? (oci-configuration-verbose? config)) + (home-service? + (oci-configuration-home-service? config))) + (if home-service? + (oci-state->shepherd-services runtime home-runtime-cli containers networks volumes + #:verbose? verbose? + #:networks-name + (oci-networks-home-shepherd-name runtime) + #:volumes-name + (oci-volumes-home-shepherd-name runtime)) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group + (oci-runtime-system-group runtime user group) + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking))))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subgids #f) + (subid-range (name user)) + subgids) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (eq? subuids #f) + (subid-range (name user)) + subuids) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + `(,bash-minimal + ,@(if (string? runtime-cli) + '() + (list + (cond + ((not (eq? runtime-cli #f)) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))))) + +(define (oci-service-extension-wrap-validate extension) + (lambda (config) + (if (oci-configuration-valid? config) + (extension config) + (raise + (formatted-message + (G_ "Invalide oci-configuration ~a.") config))))) + +(define (oci-configuration-extend config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension subids-service-type + (oci-service-extension-wrap-validate + oci-service-subids)) + (service-extension account-service-type + (oci-service-extension-wrap-validate + oci-service-accounts)) + (service-extension shepherd-root-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend oci-configuration-extend) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 828ceea313a..125e748bb0e 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -297,17 +302,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..5e6f39387e7 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Mar 2025 01:07:03 GMT) Full text and rfc822 format available.Message #125 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v8 1/5] services: rootless-podman: Use login shell. Date: Sun, 9 Mar 2025 02:06:11 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index b3cd109ce6c..d5a211765a6 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 5adfe1b8e92ff332656bcc7a9d71a35306b3411e -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Mar 2025 01:07:03 GMT) Full text and rfc822 format available.Message #128 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v8 4/5] tests: Use lower-oci-image-state in container tests. Date: Sun, 9 Mar 2025 02:06:14 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index a78be00f038..700c7b63603 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -75,6 +75,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 5e6f39387e7..8cdd86e7ae3 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
andrew <at> trop.in, janneke <at> gnu.org, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, tanguy <at> bioneland.org, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Mar 2025 01:07:04 GMT) Full text and rfc822 format available.Message #131 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v8 5/5] home: Add home-oci-service-type. Date: Sun, 9 Mar 2025 02:06:15 +0100
* gnu/home/service/containers.scm: New file; * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (OCI backed services): Document it. Change-Id: I8ce5b301e8032d0a7b2a9ca46752738cdee1f030 --- doc/guix.texi | 114 +++++++++++++++++++++++++++++++ gnu/home/services/containers.scm | 50 ++++++++++++++ gnu/local.mk | 1 + gnu/services/containers.scm | 5 ++ 4 files changed, 170 insertions(+) create mode 100644 gnu/home/services/containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index 8686380669b..7ed469f7920 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -50403,6 +50403,120 @@ Miscellaneous Home Services (dicod-configuration @dots{}))) @end lisp +@subsubheading OCI backed services + +@cindex OCI-backed, for Home +The @code{(gnu home services containers)} module provides the following service: + +@defvar home-oci-service-type +This is the type of the service that allows to manage your OCI containers with +the same consistent interface you use for your other Home Shepherd services. +@end defvar + +This service is a direct mapping of the @code{oci-service-type} system +service (@pxref{Miscellaneous Services, OCI backed services}). You can +use it like this: + +@lisp +(use-modules (gnu services containers) + (gnu home services containers)) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) + +@end lisp + +You may specify a custom configuration by providing a +@code{oci-configuration} record, exactly like for +@code{oci-service-type}, but wrapping it in @code{for-home}: + +@lisp +(use-modules (gnu services) + (gnu services containers) + (gnu home services containers)) + +(service home-oci-service-type + (for-home + (oci-configuration + (runtime 'podman) + (verbose? #t)))) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) +@end lisp + @node Invoking guix home @section Invoking @command{guix home} diff --git a/gnu/home/services/containers.scm b/gnu/home/services/containers.scm new file mode 100644 index 00000000000..938dde2f37a --- /dev/null +++ b/gnu/home/services/containers.scm @@ -0,0 +1,50 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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 home services containers) + #:use-module (gnu home services) + #:use-module (gnu home services shepherd) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services containers) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (home-oci-service-type)) + +(define home-oci-service-type + (service-type (inherit (system->home-service-type oci-service-type)) + (extensions + (list + (service-extension home-profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension home-shepherd-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + (extend + (lambda (config extension) + (for-home + (oci-configuration + (inherit (oci-configuration-extend config extension)))))) + (default-value (for-home (oci-configuration))))) diff --git a/gnu/local.mk b/gnu/local.mk index 9082ed04bfe..e0d1a25a607 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -103,6 +103,7 @@ GNU_SYSTEM_MODULES = \ %D%/home.scm \ %D%/home/services.scm \ %D%/home/services/admin.scm \ + %D%/home/services/containers.scm \ %D%/home/services/desktop.scm \ %D%/home/services/dict.scm \ %D%/home/services/dotfiles.scm \ diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 700c7b63603..002bbc1057b 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -762,6 +762,9 @@ (define-configuration/no-serialization oci-network-configuration (define (list-of-oci-networks? value) (list-of-oci-records? "networks" oci-network-configuration? value)) +;; (for-home (oci-configuration ...)) is not able to replace for-home? with #t, +;; pk prints #f. Once for-home will be able to work with (gnu services configuration) the +;; record can be migrated back to define-configuration. (define-record-type* <oci-configuration> oci-configuration make-oci-configuration @@ -796,6 +799,8 @@ (define-record-type* <oci-configuration> (define (package-or-string? value) (or (package? value) (string? value))) +;; TODO: This procedure can be dropped once we switch to define-configuration for +;; oci-configuration. (define (oci-configuration-valid? config) (define runtime-cli (oci-configuration-runtime-cli config)) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 09 Mar 2025 01:07:04 GMT) Full text and rfc822 format available.Message #134 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v8 2/5] services: oci-container-configuration: Move to (gnu services containers). Date: Sun, 9 Mar 2025 02:06:12 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. * gnu/tests/docker.scm: Simplify %test-oci-container test case. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- gnu/tests/docker.scm | 99 +++---- 3 files changed, 625 insertions(+), 600 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index d5a211765a6..24f31c756b8 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -190,7 +239,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -244,3 +293,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 9ab3e583345..828ceea313a 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -309,495 +297,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invocation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:51:02 GMT) Full text and rfc822 format available.Message #137 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Tue, 18 Mar 2025 18:50:14 +0100
Hi, On 3/9/25 01:27, paul wrote: > Hi, > > On 3/4/25 13:39, paul wrote: >> Hi, >> >> On 2/27/25 00:01, paul wrote: >>> Hi guix, >>> >>> On 2/18/25 02:20, paul wrote: >>>> Hi, >>>> >>>> On 2/12/25 02:09, paul wrote: >>>>> Hi guix, >>>>> >>>>> On 2/9/25 21:38, paul wrote: >>>>>> I'm sending a v3 fixing a bug in the merge algorithm for volumes >>>>>> and networks. >>>>>> >>>>>> On 2/9/25 20:14, paul wrote: >>>>>>> Hi, >>>>>>> >>>>>>> I'm about to send a v2. v2 compared to the first revision features: >>>>>>> >>>>>>> >>>>>>> - it actually compiles all the times :) (rev 1 referenced >>>>>>> oci-image too early for it to be working and generated a compile >>>>>>> time error, if you recompiled it sometimes went away so I >>>>>>> thought it was a problem of my setup. CI caught this) >>>>>>> - it allows more values to be overridden by eventual users of >>>>>>> the Scheme API >>>>>>> - it allows passing extra arguments directly after each podman >>>>>>> or docker invokation, allowing for example for overriding podman >>>>>>> --root and similar options. >>>>>>> >>>>>>> All of these tests should pass: >>>>>>> >>>>>>> guix shell -D guix -CPW -- make check-system >>>>>>> TESTS="oci-container oci-service-rootless-podman docker >>>>>>> docker-system rootless-podman oci-service-docker" >>>>>>> >>>>> I'm sending a v4 changing slightly the image loader, the same >>>>> tests as before are supposed to pass. Now the Home service [0] is >>>>> working for me with rootless podman. I'll try it on different >>>>> distros if I manage to. >>>> >>>> I'm sending a v5 implementing a Home service. The changes compared >>>> to v4 are pretty trivial as the plumbing was already there, the >>>> only downside is that I'm not able to use for-home? in >>>> define-configuration, so I had to reimplement oci-configuration >>>> with (guix records) and had to reimplement some validation (gnu >>>> services configuration) would figure out magically. >>> >>> I'm sending a v6. Compared to v5 it resolves the conflicts with >>> master and it should fix the stop action for rootless podman backed >>> services. >> >> I'm sending a v7 fixing the restart action for podman backed services. > > I'm sending a v8 fixing some further bugs wrt to the start and stop of > podman backed services. In the hope of making the review easier, I'm sending a v9 that is completely the same of v8 in terms of changes but with smaller commits. Hopefully this makes it easier to review changes, please let me know if I can do anything to ease the workload for reviewers of this patch set. Thank you for your work, cheers giacomo
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:53:03 GMT) Full text and rfc822 format available.Message #140 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v9 3/7] tests: oci-container: Set explicit timeouts. Date: Tue, 18 Mar 2025 18:51:08 +0100
* gnu/tests/docker.scm: Simplify %test-oci-container test case and add explicit timeouts to tests outcomes. --- gnu/tests/docker.scm | 99 ++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.48.1
ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:53:06 GMT) Full text and rfc822 format available.Message #143 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v9 4/7] services: Add oci-service-type. Date: Tue, 18 Mar 2025 18:51:09 +0100
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 306 +++++++-- gnu/services/containers.scm | 1300 ++++++++++++++++++++++++++++++----- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 ++++++++++++++++++++++++++- 4 files changed, 2390 insertions(+), 252 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 04885593328..521ea28dd5a 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -42914,59 +42914,162 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package-or-string) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources, it can be either a package or a string representing +an absolute path to the runtime binary entrypoint. When unset it will default +to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} +package for the @code{'podman} runtime. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -42986,16 +43089,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -43003,22 +43106,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -43046,7 +43151,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -43057,10 +43162,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -43071,25 +43177,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 24f31c756b8..d1ebfa1736c 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -41,6 +41,7 @@ (define-module (gnu services containers) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +97,78 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-home-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + oci-configuration-valid? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-container-shepherd-name + oci-networks-shepherd-name + oci-networks-home-shepherd-name + oci-volumes-shepherd-name + oci-volumes-home-shepherd-name + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-extension-merge + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services + oci-configuration-extend)) (define (gexp-or-string? value) (or (gexp? value) @@ -296,9 +367,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (not (maybe-value-set? maybe-group)) + (if (eq? 'podman runtime) + "cgroup" + "docker") + maybe-group)) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -347,6 +451,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -374,10 +483,15 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -392,7 +506,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -437,11 +563,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -451,9 +581,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -477,15 +607,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -514,7 +645,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -526,9 +657,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -541,71 +673,348 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invocation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define (package-or-string? value) + (or (package? value) (string? value))) + +(define-maybe/no-serialization package-or-string) + +(define-configuration/no-serialization oci-configuration + (runtime + (symbol 'docker) + "The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}." + (sanitizer oci-sanitize-runtime)) + (runtime-cli + (maybe-package-or-string) + "The OCI runtime command line to be installed in the system profile and used +to provision OCI resources, it can be either a package or a string representing +an absolute path to the runtime binary entrypoint. When unset it will default +to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} +package for the @code{'podman} runtime.") + (runtime-extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation.") + (user + (string "oci-container") + "The user name under whose authority OCI runtime commands will be run.") + (group + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") + (subuids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (subgids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (verbose? + (boolean #f) + "When true, additional output will be printed, allowing to better follow the +flow of execution.")) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (not (maybe-value-set? runtime-cli)) + path + runtime-cli) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define (oci-runtime-home-cli config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli + (string-append (getenv "HOME") + "/.guix-home/profile")))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invocation) + "Return a Shepherd action printing a given INVOCATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invocation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invocation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-networks-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-networks-shepherd-name runtime))) + +(define (oci-volumes-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-volumes-shepherd-name runtime))) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -614,24 +1023,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -642,14 +1038,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -662,7 +1055,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -677,113 +1071,669 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define (oci-object-exists? runtime runtime-cli object verbose?) + #~(lambda* (name #:key (format-string "{{.Name}}")) + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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))) + + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"" format-string "\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command))))))) + +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) + (program-file + (format #f "~a-image-loader" name) #~(begin (use-modules (guix build utils) + (ice-9 match) (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) + (ice-9 rdelim) + (srfi srfi-1)) + (define object-exists? + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") + (format #t "~a image already exists, skipping.~%" #$tag) + (begin + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (when #$verbose? + (format #t "Running ~a~%" load-command)) + (let ((line (read-line + (open-input-pipe load-command)))) + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag)) + (drop-old-tag-command + (list #$runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag #$tag) + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) + + (let ((exit-code + (status:exit-val (apply system* tag-command)))) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) + + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + + (when (equal? EXIT_SUCCESS exit-code) + (when #$verbose? + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) + (let ((drop-exit-code + (status:exit-val (apply system* drop-old-tag-command)))) + (when #$verbose? + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) + +(define (oci-container-run-invocation runtime runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invocation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" "--rm" + ,@(if (eq? runtime 'podman) + ;; This is because podman takes some time to + ;; release container names. --replace seems + ;; to be required to be able to restart services. + '("--replace") + '()) + "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference + invocation #:key (verbose? #f) (pre-script #~())) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%") + (format #t "Current user: ~a ~a~%" + (getuid) (passwd:name (getpwuid (getuid)))) + (format #t "Current group: ~a ~a~%" + (getgid) (group:name (getgrgid (getgid)))) + (format #t "Current directory ~a~%" (getcwd))) + (define invocation (list #$@invocation)) + #$@pre-script + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (apply execlp `(,(first invocation) ,@invocation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (maybe-user (oci-container-configuration-user config)) + (maybe-group (oci-container-configuration-group config)) + (user (if (maybe-value-set? maybe-user) + maybe-user + user)) + (group (if (maybe-value-set? maybe-group) + maybe-group + group)) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invocation + (oci-container-run-invocation + runtime runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments)) + (container-action + (lambda* (command #:key (environment-variables #f)) + #~(lambda _ + (fork+exec-command + (list #$@command) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #$@(if (and user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) + #$@(if environment-variables + (list #:environment-variables + environment-variables) + '())))))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) + (container-action + (list (oci-container-entrypoint + runtime runtime-cli name image image-reference + invocation #:verbose? verbose? + #:pre-script + (if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime runtime-cli name image + image-reference #:verbose? verbose?))) + #~()))) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (container-action + (list + (oci-container-entrypoint + runtime runtime-cli name image image-reference + (list runtime-cli "rm" "-f" name) + #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invocation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + (container-action + (list + (oci-container-entrypoint + runtime runtime-cli service-name image + image-reference + (list runtime-cli "pull" image) + #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))))))) + actions))))) + +(define (oci-object-create-invocation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invocation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invocations invocations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOCATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invocations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invocations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-1)) + + (define object-exists? + #$(oci-object-exists? runtime runtime-cli object verbose?)) + + (for-each + (lambda (invocation) + (define name (last invocation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (let ((exit-code (status:exit-val (apply system* invocation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invocations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invocations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOCATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invocations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invocations invocations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invocations + (map + (lambda (network) + (oci-object-create-invocation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invocations + (map + (lambda (volume) + (oci-object-create-invocation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + "Returns a list of Shepherd services based on the input OCI state." + (let* ((networks-name + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime))) + (networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol networks-name)) + '())) + (volumes-name + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime))) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list (string->symbol volumes-name)) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + networks-name networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + volumes-name volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (system-runtime-cli + (oci-runtime-system-cli config)) + (home-runtime-cli + (oci-runtime-home-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + (oci-runtime-group config (oci-configuration-group config)))) + (verbose? (oci-configuration-verbose? config))) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group group + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking)))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (not (maybe-value-set? subgids)) + (subid-range (name user)) + subgids) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (not (maybe-value-set? subuids)) + (subid-range (name user)) + subuids) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + `(,bash-minimal + ,@(if (string? runtime-cli) + '() + (list + (cond + ((maybe-value-set? runtime-cli) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))))) + +(define (oci-configuration-extend config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension subids-service-type + oci-service-subids) + (service-extension account-service-type + oci-service-accounts) + (service-extension shepherd-root-service-type + oci-configuration->shepherd-services))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend oci-configuration-extend) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 828ceea313a..125e748bb0e 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -297,17 +302,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..5e6f39387e7 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:53:09 GMT) Full text and rfc822 format available.Message #146 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v9 5/7] tests: Use lower-oci-image-state in container tests. Date: Tue, 18 Mar 2025 18:51:10 +0100
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index d1ebfa1736c..5dac8e80f22 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -74,6 +74,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 5e6f39387e7..8cdd86e7ae3 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:53:11 GMT) Full text and rfc822 format available.Message #149 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v9 1/7] services: rootless-podman: Use login shell. Date: Tue, 18 Mar 2025 18:51:06 +0100
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index b3cd109ce6c..d5a211765a6 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 9879a7d75279762c7634f1a585794442ea2f0503 -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:53:12 GMT) Full text and rfc822 format available.Message #152 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v9 2/7] services: oci-container-configuration: Move to (gnu services containers). Date: Tue, 18 Mar 2025 18:51:07 +0100
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- 2 files changed, 584 insertions(+), 542 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index d5a211765a6..24f31c756b8 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -190,7 +239,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -244,3 +293,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 9ab3e583345..828ceea313a 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -309,495 +297,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invocation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) -- 2.48.1
andrew <at> trop.in, janneke <at> gnu.org, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, tanguy <at> bioneland.org, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:53:12 GMT) Full text and rfc822 format available.Message #155 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v9 7/7] home: Add home-oci-service-type. Date: Tue, 18 Mar 2025 18:51:12 +0100
* gnu/home/service/containers.scm: New file; * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (OCI backed services): Document it. Change-Id: I8ce5b301e8032d0a7b2a9ca46752738cdee1f030 --- doc/guix.texi | 114 +++++++++++++++++++++++++++++++ gnu/home/services/containers.scm | 50 ++++++++++++++ gnu/local.mk | 1 + 3 files changed, 165 insertions(+) create mode 100644 gnu/home/services/containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index 521ea28dd5a..8f3017c2f69 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -50478,6 +50478,120 @@ Miscellaneous Home Services (dicod-configuration @dots{}))) @end lisp +@subsubheading OCI backed services + +@cindex OCI-backed, for Home +The @code{(gnu home services containers)} module provides the following service: + +@defvar home-oci-service-type +This is the type of the service that allows to manage your OCI containers with +the same consistent interface you use for your other Home Shepherd services. +@end defvar + +This service is a direct mapping of the @code{oci-service-type} system +service (@pxref{Miscellaneous Services, OCI backed services}). You can +use it like this: + +@lisp +(use-modules (gnu services containers) + (gnu home services containers)) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) + +@end lisp + +You may specify a custom configuration by providing a +@code{oci-configuration} record, exactly like for +@code{oci-service-type}, but wrapping it in @code{for-home}: + +@lisp +(use-modules (gnu services) + (gnu services containers) + (gnu home services containers)) + +(service home-oci-service-type + (for-home + (oci-configuration + (runtime 'podman) + (verbose? #t)))) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) +@end lisp + @node Invoking guix home @section Invoking @command{guix home} diff --git a/gnu/home/services/containers.scm b/gnu/home/services/containers.scm new file mode 100644 index 00000000000..938dde2f37a --- /dev/null +++ b/gnu/home/services/containers.scm @@ -0,0 +1,50 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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 home services containers) + #:use-module (gnu home services) + #:use-module (gnu home services shepherd) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services containers) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (home-oci-service-type)) + +(define home-oci-service-type + (service-type (inherit (system->home-service-type oci-service-type)) + (extensions + (list + (service-extension home-profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension home-shepherd-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + (extend + (lambda (config extension) + (for-home + (oci-configuration + (inherit (oci-configuration-extend config extension)))))) + (default-value (for-home (oci-configuration))))) diff --git a/gnu/local.mk b/gnu/local.mk index 01d13a11ae8..0f6502d979b 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -103,6 +103,7 @@ GNU_SYSTEM_MODULES = \ %D%/home.scm \ %D%/home/services.scm \ %D%/home/services/admin.scm \ + %D%/home/services/containers.scm \ %D%/home/services/desktop.scm \ %D%/home/services/dict.scm \ %D%/home/services/dotfiles.scm \ -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 18 Mar 2025 17:53:13 GMT) Full text and rfc822 format available.Message #158 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v9 6/7] services: oci: Migrate oci-configuration to (guix records). Date: Tue, 18 Mar 2025 18:51:11 +0100
This commit migrates oci-configuration to (guix records) singe it appears (for-home (oci-configuration ...)) does not work as expected with (gnu services configuration). This is supposed to be completely transparent for users and can be reverted in the future once this has been implemented. * gnu/service/containers.scm: Migrate oci-configuration to (guix records). --- gnu/services/containers.scm | 199 +++++++++++++++++++++--------------- 1 file changed, 117 insertions(+), 82 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 5dac8e80f22..7a44e1c0f7c 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -39,6 +39,7 @@ (define-module (gnu services containers) #:use-module (guix packages) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix records) #:use-module (guix store) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -164,6 +165,7 @@ (define-module (gnu services containers) oci-container-shepherd-service oci-objects-merge-lst oci-extension-merge + oci-service-extension-wrap-validate oci-service-type oci-service-accounts oci-service-profile @@ -391,7 +393,7 @@ (define (oci-runtime-name runtime) (define (oci-runtime-group runtime maybe-group) "Implement the logic behind selection of the group that is to be used by Shepherd to execute OCI commands." - (if (not (maybe-value-set? maybe-group)) + (if (eq? maybe-group #f) (if (eq? 'podman runtime) "cgroup" "docker") @@ -762,62 +764,74 @@ (define (list-of-oci-networks? value) (define (package-or-string? value) (or (package? value) (string? value))) -(define-maybe/no-serialization package-or-string) - -(define-configuration/no-serialization oci-configuration - (runtime - (symbol 'docker) - "The OCI runtime to use to run commands. It can be either @code{'docker} or -@code{'podman}." - (sanitizer oci-sanitize-runtime)) - (runtime-cli - (maybe-package-or-string) - "The OCI runtime command line to be installed in the system profile and used -to provision OCI resources, it can be either a package or a string representing -an absolute path to the runtime binary entrypoint. When unset it will default -to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} -package for the @code{'podman} runtime.") - (runtime-extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be placed -after each @command{docker} or @command{podman} invokation.") - (user - (string "oci-container") - "The user name under whose authority OCI runtime commands will be run.") - (group - (maybe-string) - "The group name under whose authority OCI commands will be run. When -using the @code{'podman} OCI runtime, this field will be ignored and the -default group of the user configured in the @code{user} field will be used.") - (subuids-range - (maybe-subid-range) - "An optional @code{subid-range} record allocating subuids for the user from -the @code{user} field. When unset, with the rootless Podman OCI runtime, it -defaults to @code{(subid-range (name \"oci-container\"))}.") - (subgids-range - (maybe-subid-range) - "An optional @code{subid-range} record allocating subgids for the user from -the @code{user} field. When unset, with the rootless Podman OCI runtime, it -defaults to @code{(subid-range (name \"oci-container\"))}.") - (containers - (list-of-oci-containers '()) - "The list of @code{oci-container-configuration} records representing the -containers to provision. Most users are supposed not to use this field and use -the @code{oci-extension} record instead.") - (networks - (list-of-oci-networks '()) - "The list of @code{oci-network-configuration} records representing the -networks to provision. Most users are supposed not to use this field and use -the @code{oci-extension} record instead.") - (volumes - (list-of-oci-volumes '()) - "The list of @code{oci-volume-configuration} records representing the -volumes to provision. Most users are supposed not to use this field and use -the @code{oci-extension} record instead.") - (verbose? - (boolean #f) - "When true, additional output will be printed, allowing to better follow the -flow of execution.")) +;; (for-home (oci-configuration ...)) is not able to replace for-home? with #t, +;; pk prints #f. Once for-home will be able to work with (gnu services configuration) the +;; record can be migrated back to define-configuration. +(define-record-type* <oci-configuration> + oci-configuration + make-oci-configuration + oci-configuration? + this-oci-configuration + + (runtime oci-configuration-runtime + (default 'docker)) + (runtime-cli oci-configuration-runtime-cli + (default #f)) ; package or string + (runtime-extra-arguments oci-configuration-runtime-extra-arguments ; strings or gexps + (default '())) ; or file-like objects + (user oci-configuration-user + (default "oci-container")) + (group oci-configuration-group ; string + (default #f)) + (subuids-range oci-configuration-subuids-range ; subid-range + (default #f)) + (subgids-range oci-configuration-subgids-range ; subid-range + (default #f)) + (containers oci-configuration-containers ; oci-container-configurations + (default '())) + (networks oci-configuration-networks ; oci-network-configurations + (default '())) + (volumes oci-configuration-volumes ; oci-volume-configurations + (default '())) + (verbose? oci-configuration-verbose? + (default #f)) + (home-service? oci-configuration-home-service? + (default for-home?) (innate))) + +;; TODO: This procedure can be dropped once we switch to define-configuration for +;; oci-configuration. +(define (oci-configuration-valid? config) + (define runtime-cli + (oci-configuration-runtime-cli config)) + (define group + (oci-configuration-group config)) + (define subuids-range + (oci-configuration-subuids-range config)) + (define subgids-range + (oci-configuration-subgids-range config)) + (and + (symbol? + (oci-sanitize-runtime (oci-configuration-runtime config))) + (or (eq? runtime-cli #f) + (package-or-string? runtime-cli)) + (list? (oci-configuration-runtime-extra-arguments config)) + (string? (oci-configuration-user config)) + (or (eq? group #f) + (string? group)) + (or (eq? subuids-range #f) + (subid-range? subuids-range)) + (or (eq? subgids-range #f) + (subid-range? subgids-range)) + (list-of-oci-containers? + (oci-configuration-containers config)) + (list-of-oci-networks? + (oci-configuration-networks config)) + (list-of-oci-volumes? + (oci-configuration-volumes config)) + (boolean? + (oci-configuration-verbose? config)) + (boolean? + (oci-configuration-home-service? config)))) (define (oci-runtime-system-environment runtime user) (if (eq? runtime 'podman) @@ -833,7 +847,7 @@ (define (oci-runtime-cli runtime runtime-cli path) ;; It is a user defined absolute path runtime-cli #~(string-append - #$(if (not (maybe-value-set? runtime-cli)) + #$(if (eq? runtime-cli #f) path runtime-cli) #$(if (eq? 'podman runtime) @@ -1577,18 +1591,27 @@ (define (oci-configuration->shepherd-services config) (passwd:gid (getpwnam #$user)))) (oci-runtime-group config (oci-configuration-group config)))) - (verbose? (oci-configuration-verbose? config))) - (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes - #:user user - #:group group - #:verbose? verbose? - #:runtime-extra-arguments - runtime-extra-arguments - #:runtime-environment - (oci-runtime-system-environment runtime user) - #:runtime-requirement - (oci-runtime-system-requirement runtime) - #:networks-requirement '(networking)))) + (verbose? (oci-configuration-verbose? config)) + (home-service? + (oci-configuration-home-service? config))) + (if home-service? + (oci-state->shepherd-services runtime home-runtime-cli containers networks volumes + #:verbose? verbose? + #:networks-name + (oci-networks-home-shepherd-name runtime) + #:volumes-name + (oci-volumes-home-shepherd-name runtime)) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group group + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking))))) (define (oci-service-subids config) "Return a subids-extension record representing subuids and subgids required by @@ -1616,14 +1639,14 @@ (define (oci-service-subids config) (define subgid-ranges (delete-duplicate-ranges (cons - (if (not (maybe-value-set? subgids)) + (if (eq? subgids #f) (subid-range (name user)) subgids) container-users))) (define subuid-ranges (delete-duplicate-ranges (cons - (if (not (maybe-value-set? subuids)) + (if (eq? subuids #f) (subid-range (name user)) subuids) container-users))) @@ -1682,13 +1705,21 @@ (define (oci-service-profile runtime runtime-cli) '() (list (cond - ((maybe-value-set? runtime-cli) + ((not (eq? runtime-cli #f)) runtime-cli) ((eq? 'podman runtime) podman) (else docker-cli)))))) +(define (oci-service-extension-wrap-validate extension) + (lambda (config) + (if (oci-configuration-valid? config) + (extension config) + (raise + (formatted-message + (G_ "Invalide oci-configuration ~a.") config))))) + (define (oci-configuration-extend config extension) (oci-configuration (inherit config) @@ -1717,18 +1748,22 @@ (define oci-service-type (extensions (list (service-extension profile-service-type - (lambda (config) - (let ((runtime-cli - (oci-configuration-runtime-cli config)) - (runtime - (oci-configuration-runtime config))) - (oci-service-profile runtime runtime-cli)))) + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) (service-extension subids-service-type - oci-service-subids) + (oci-service-extension-wrap-validate + oci-service-subids)) (service-extension account-service-type - oci-service-accounts) + (oci-service-extension-wrap-validate + oci-service-accounts)) (service-extension shepherd-root-service-type - oci-configuration->shepherd-services))) + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) ;; Concatenate OCI object lists. (compose (lambda (args) (fold oci-extension-merge -- 2.48.1
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:55:02 GMT) Full text and rfc822 format available.Message #161 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Subject: Re: OCI provisioning service Date: Mon, 5 May 2025 09:54:49 +0200
Hi, On 3/18/25 18:50, paul wrote: > Hi, > > On 3/9/25 01:27, paul wrote: >> Hi, >> >> On 3/4/25 13:39, paul wrote: >>> Hi, >>> >>> On 2/27/25 00:01, paul wrote: >>>> Hi guix, >>>> >>>> On 2/18/25 02:20, paul wrote: >>>>> Hi, >>>>> >>>>> On 2/12/25 02:09, paul wrote: >>>>>> Hi guix, >>>>>> >>>>>> On 2/9/25 21:38, paul wrote: >>>>>>> I'm sending a v3 fixing a bug in the merge algorithm for volumes >>>>>>> and networks. >>>>>>> >>>>>>> On 2/9/25 20:14, paul wrote: >>>>>>>> Hi, >>>>>>>> >>>>>>>> I'm about to send a v2. v2 compared to the first revision >>>>>>>> features: >>>>>>>> >>>>>>>> >>>>>>>> - it actually compiles all the times :) (rev 1 referenced >>>>>>>> oci-image too early for it to be working and generated a >>>>>>>> compile time error, if you recompiled it sometimes went away so >>>>>>>> I thought it was a problem of my setup. CI caught this) >>>>>>>> - it allows more values to be overridden by eventual users of >>>>>>>> the Scheme API >>>>>>>> - it allows passing extra arguments directly after each podman >>>>>>>> or docker invokation, allowing for example for overriding >>>>>>>> podman --root and similar options. >>>>>>>> >>>>>>>> All of these tests should pass: >>>>>>>> >>>>>>>> guix shell -D guix -CPW -- make check-system >>>>>>>> TESTS="oci-container oci-service-rootless-podman docker >>>>>>>> docker-system rootless-podman oci-service-docker" >>>>>>>> >>>>>> I'm sending a v4 changing slightly the image loader, the same >>>>>> tests as before are supposed to pass. Now the Home service [0] is >>>>>> working for me with rootless podman. I'll try it on different >>>>>> distros if I manage to. >>>>> >>>>> I'm sending a v5 implementing a Home service. The changes compared >>>>> to v4 are pretty trivial as the plumbing was already there, the >>>>> only downside is that I'm not able to use for-home? in >>>>> define-configuration, so I had to reimplement oci-configuration >>>>> with (guix records) and had to reimplement some validation (gnu >>>>> services configuration) would figure out magically. >>>> >>>> I'm sending a v6. Compared to v5 it resolves the conflicts with >>>> master and it should fix the stop action for rootless podman backed >>>> services. >>> >>> I'm sending a v7 fixing the restart action for podman backed services. >> >> I'm sending a v8 fixing some further bugs wrt to the start and stop >> of podman backed services. > > In the hope of making the review easier, I'm sending a v9 that is > completely the same of v8 in terms of changes but with smaller > commits. Hopefully this makes it easier to review changes, please let > me know if I can do anything to ease the workload for reviewers of > this patch set. I'm sending a v10 rebased on current master. The only difference with v9 is that this revision exports the oci-{container,network,volume}-configuration->options procedures , that can be useful when writing services that extend the oci-service-type. cheers, giacomo
gabriel <at> erlikon.ch, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:59:02 GMT) Full text and rfc822 format available.Message #164 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v10 4/7] services: Add oci-service-type. Date: Mon, 5 May 2025 09:57:51 +0200
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/services/containers.scm (%oci-supported-runtimes): New variable; (oci-runtime-cli): new variable; (oci-runtime-name): new variable; (oci-network-configuration): new variable; (oci-volume-configuration): new variable; (oci-configuration): new variable; (oci-extension): new variable; (oci-networks-shepherd-name): new variable; (oci-service-type): new variable; (oci-state->shepherd-services): new variable. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 --- doc/guix.texi | 306 ++++++-- gnu/services/containers.scm | 1304 ++++++++++++++++++++++++++++++----- gnu/services/docker.scm | 37 +- gnu/tests/containers.scm | 999 ++++++++++++++++++++++++++- 4 files changed, 2394 insertions(+), 252 deletions(-) diff --git a/doc/guix.texi b/doc/guix.texi index 7f796c5fc94..de0d7ccfd5a 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -43782,59 +43782,162 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@command{docker run} or @command{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package-or-string) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources, it can be either a package or a string representing +an absolute path to the runtime binary entrypoint. When unset it will default +to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} +package for the @code{'podman} runtime. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -43854,16 +43957,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @command{docker run} +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -43871,22 +43974,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -43914,7 +44019,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -43925,10 +44030,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -43939,25 +44045,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} or @command{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invokation. @end table diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 24f31c756b8..66b46456800 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -41,6 +41,7 @@ (define-module (gnu services containers) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +97,82 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-sanitize-runtime + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-home-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + oci-configuration-valid? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-container-shepherd-name + oci-networks-shepherd-name + oci-networks-home-shepherd-name + oci-volumes-shepherd-name + oci-volumes-home-shepherd-name + + oci-container-configuration->options + oci-network-configuration->options + oci-volume-configuration->options + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-extension-merge + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-state->shepherd-services + oci-configuration->shepherd-services + oci-configuration-extend)) (define (gexp-or-string? value) (or (gexp? value) @@ -296,9 +371,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (not (maybe-value-set? maybe-group)) + (if (eq? 'podman runtime) + "cgroup" + "docker") + maybe-group)) + +(define (oci-sanitize-runtime value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + value) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -347,6 +455,11 @@ (define (oci-sanitize-volumes value) ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -374,10 +487,15 @@ (define (oci-sanitize-extra-arguments value) value)) (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -392,7 +510,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -437,11 +567,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -451,9 +585,9 @@ (define-configuration/no-serialization oci-container-configuration (host-environment (list '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -477,15 +611,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -514,7 +649,7 @@ (define-configuration/no-serialization oci-container-configuration (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports (list '()) "Set the port or port ranges to expose from the spawned container. This can @@ -526,9 +661,10 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes (list '()) @@ -541,71 +677,348 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments (list '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invocation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (list '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define (package-or-string? value) + (or (package? value) (string? value))) + +(define-maybe/no-serialization package-or-string) + +(define-configuration/no-serialization oci-configuration + (runtime + (symbol 'docker) + "The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}." + (sanitizer oci-sanitize-runtime)) + (runtime-cli + (maybe-package-or-string) + "The OCI runtime command line to be installed in the system profile and used +to provision OCI resources, it can be either a package or a string representing +an absolute path to the runtime binary entrypoint. When unset it will default +to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} +package for the @code{'podman} runtime.") + (runtime-extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation.") + (user + (string "oci-container") + "The user name under whose authority OCI runtime commands will be run.") + (group + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") + (subuids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (subgids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to provision. Most users are supposed not to use this field and use +the @code{oci-extension} record instead.") + (verbose? + (boolean #f) + "When true, additional output will be printed, allowing to better follow the +flow of execution.")) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-cli runtime runtime-cli path) + "Return a gexp that, when lowered, evaluates to the file system path of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute path + runtime-cli + #~(string-append + #$(if (not (maybe-value-set? runtime-cli)) + path + runtime-cli) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli path))) + +(define (oci-runtime-home-cli config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli + (string-append (getenv "HOME") + "/.guix-home/profile")))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-object-command-shepherd-action object-name invocation) + "Return a Shepherd action printing a given INVOCATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invocation." + object-name)) + (procedure + #~(lambda _ + (format #t "~a~%" #$invocation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-networks-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-networks-shepherd-name runtime))) + +(define (oci-volumes-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-volumes-shepherd-name runtime))) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -614,24 +1027,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -642,14 +1042,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -662,7 +1059,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -677,113 +1075,669 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define (oci-object-exists? runtime runtime-cli object verbose?) + #~(lambda* (name #:key (format-string "{{.Name}}")) + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-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))) + + #$(if (eq? runtime 'podman) + #~(let ((command + (list #$runtime-cli + #$object "exists" name))) + (when #$verbose? + (format #t "Running~{ ~a~}~%" command)) + (define exit-code (status:exit-val (apply system* command))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + (equal? EXIT_SUCCESS exit-code)) + #~(let ((command + (string-append #$runtime-cli + " " #$object " ls --format " + "\"" format-string "\""))) + (when #$verbose? + (format #t "Running ~a~%" command)) + (member name (read-lines (open-input-pipe command))))))) + +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) + (program-file + (format #f "~a-image-loader" name) #~(begin (use-modules (guix build utils) + (ice-9 match) (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) + (ice-9 rdelim) + (srfi srfi-1)) + (define object-exists? + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) + (define load-command + (string-append #$runtime-cli + " load -i " #$tarball)) + + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") + (format #t "~a image already exists, skipping.~%" #$tag) + (begin + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (when #$verbose? + (format #t "Running ~a~%" load-command)) + (let ((line (read-line + (open-input-pipe load-command)))) + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-command + (list #$runtime-cli "tag" repository&tag #$tag)) + (drop-old-tag-command + (list #$runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag #$tag) + (when #$verbose? + (format #t "Running~{ ~a~}~%" tag-command)) + + (let ((exit-code + (status:exit-val (apply system* tag-command)))) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) + + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)) + + (when (equal? EXIT_SUCCESS exit-code) + (when #$verbose? + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) + (let ((drop-exit-code + (status:exit-val (apply system* drop-old-tag-command)))) + (when #$verbose? + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) + +(define (oci-container-run-invocation runtime runtime-cli name command image-reference + options runtime-extra-arguments run-extra-arguments) + "Return a list representing the OCI runtime +invocation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,runtime-cli ,@runtime-extra-arguments "run" "--rm" + ,@(if (eq? runtime 'podman) + ;; This is because podman takes some time to + ;; release container names. --replace seems + ;; to be required to be able to restart services. + '("--replace") + '()) + "--name" ,name + ,@options ,@run-extra-arguments + ,image-reference ,@command)) + +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference + invocation #:key (verbose? #f) (pre-script #~())) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run IMAGE through RUNTIME-CLI." + (program-file + (string-append "oci-entrypoint-" name) + #~(begin + (use-modules (ice-9 format) + (srfi srfi-1)) + (when #$verbose? + (format #t "Running in verbose mode...~%") + (format #t "Current user: ~a ~a~%" + (getuid) (passwd:name (getpwuid (getuid)))) + (format #t "Current group: ~a ~a~%" + (getgid) (group:name (getgrgid (getgid)))) + (format #t "Current directory ~a~%" (getcwd))) + (define invocation (list #$@invocation)) + #$@pre-script + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (apply execlp `(,(first invocation) ,@invocation))))) + +(define* (oci-container-shepherd-service runtime runtime-cli config + #:key + (runtime-environment #~()) + (runtime-extra-arguments '()) + (oci-requirement '()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (let* ((actions (oci-container-configuration-shepherd-actions config)) (auto-start? (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) + (maybe-user (oci-container-configuration-user config)) + (maybe-group (oci-container-configuration-group config)) + (user (if (maybe-value-set? maybe-user) + maybe-user + user)) + (group (if (maybe-value-set? maybe-group) + maybe-group + group)) (host-environment (oci-container-configuration-host-environment config)) (command (oci-container-configuration-command config)) (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) (requirement (oci-container-configuration-requirement config)) (respawn? (oci-container-configuration-respawn? config)) (image (oci-container-configuration-image config)) (image-reference (oci-image-reference image)) (options (oci-container-configuration->options config)) - (name (guess-name provision image)) + (name + (oci-container-shepherd-name runtime config)) (extra-arguments - (oci-container-configuration-extra-arguments config))) + (oci-container-configuration-extra-arguments config)) + (invocation + (oci-container-run-invocation + runtime runtime-cli name command image-reference + options runtime-extra-arguments extra-arguments)) + (container-action + (lambda* (command #:key (environment-variables #f)) + #~(lambda _ + (fork+exec-command + (list #$@command) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #$@(if (and user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir (getpwnam #$user))) + '()) + #$@(if environment-variables + (list #:environment-variables + environment-variables) + '())))))) (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (requirement `(,@oci-requirement + ,@requirement)) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " + (oci-runtime-name runtime) " backed Shepherd service for " (if (oci-image? image) name image) ".")) (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) + (container-action + (list (oci-container-entrypoint + runtime runtime-cli name image image-reference + invocation #:verbose? verbose? + #:pre-script + (if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime runtime-cli name image + image-reference #:verbose? verbose?))) + #~()))) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (container-action + (list + (oci-container-entrypoint + runtime runtime-cli name image image-reference + (list runtime-cli "rm" "-f" name) + #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-object-command-shepherd-action + name #~(string-join (list #$@invocation) " "))) + (if (oci-image? image) + '() (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (let ((service-name name)) + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + service-name image)) + (procedure + (container-action + (list + (oci-container-entrypoint + runtime runtime-cli service-name image + image-reference + (list runtime-cli "pull" image) + #:verbose? verbose?)) + #:environment-variables + #~(append + (list #$@host-environment) + (list #$@runtime-environment)))))))) + actions))))) + +(define (oci-object-create-invocation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invocation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invocations invocations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOCATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invocations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invocations + #:key (verbose? #f)) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (ice-9 format) + (ice-9 match) + (ice-9 popen) + (ice-9 rdelim) + (srfi srfi-1)) + + (define object-exists? + #$(oci-object-exists? runtime runtime-cli object verbose?)) + + (for-each + (lambda (invocation) + (define name (last invocation)) + (if (object-exists? name) + (format #t "~a ~a ~a already exists, skipping creation.~%" + #$(oci-runtime-name runtime) name #$object) + (begin + (when #$verbose? + (format #t "Running~{ ~a~}~%" invocation)) + (let ((exit-code (status:exit-val (apply system* invocation)))) + (when #$verbose? + (format #t "Exit code: ~a~%" exit-code)))))) + (list #$@invocations))))) + +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invocations + #:key + (runtime-environment #~()) + (user #f) + (group #f) + (verbose? #f)) + "Return a Shepherd service object that will create the OBJECTs represented +by INVOCATIONS through RUNTIME-CLI." + (shepherd-service (provision `(,(string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list + #$(oci-object-create-script + object runtime runtime-cli + invocations + #:verbose? verbose?)) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-object-command-shepherd-action + name (format-oci-invocations invocations)))))) + +(define* (oci-networks-shepherd-service runtime runtime-cli name networks + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the networks represented +in CONFIG." + (let ((invocations + (map + (lambda (network) + (oci-object-create-invocation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + runtime-extra-arguments + (oci-network-configuration-extra-arguments network))) + networks))) + + (oci-object-shepherd-service + "network" runtime runtime-cli name + runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define* (oci-volumes-shepherd-service runtime runtime-cli name volumes + #:key (user #f) (group #f) (verbose? #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '())) + "Return a Shepherd service object that will create the volumes represented +in CONFIG." + (let ((invocations + (map + (lambda (volume) + (oci-object-create-invocation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + runtime-extra-arguments + (oci-volume-configuration-extra-arguments volume))) + volumes))) + + (oci-object-shepherd-service + "volume" runtime runtime-cli name runtime-requirement invocations + #:user user #:group group #:runtime-environment runtime-environment + #:verbose? verbose?))) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services runtime runtime-cli containers networks volumes + #:key (user #f) (group #f) (verbose? #f) + (networks-name #f) (volumes-name #f) + (runtime-extra-arguments '()) + (runtime-environment #~()) + (runtime-requirement '()) + (containers-requirement '()) + (networks-requirement '()) + (volumes-requirement '())) + "Returns a list of Shepherd services based on the input OCI state." + (let* ((networks-name + (if (string? networks-name) + networks-name + (oci-networks-shepherd-name runtime))) + (networks? + (> (length networks) 0)) + (networks-service + (if networks? + (list + (string->symbol networks-name)) + '())) + (volumes-name + (if (string? volumes-name) + volumes-name + (oci-volumes-shepherd-name runtime))) + (volumes? + (> (length volumes) 0)) + (volumes-service + (if volumes? + (list (string->symbol volumes-name)) + '()))) + (append + (map + (lambda (c) + (oci-container-shepherd-service + runtime runtime-cli c + #:user user + #:group group + #:runtime-environment runtime-environment + #:runtime-extra-arguments runtime-extra-arguments + #:oci-requirement + (append containers-requirement + runtime-requirement + networks-service + volumes-service) + #:verbose? verbose?)) + containers) + (if networks? + (list + (oci-networks-shepherd-service + runtime runtime-cli + networks-name networks + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append networks-requirement + runtime-requirement) + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + runtime runtime-cli + volumes-name volumes + #:user user #:group group + #:runtime-extra-arguments runtime-extra-arguments + #:runtime-environment runtime-environment + #:runtime-requirement (append runtime-requirement + volumes-requirement) + #:verbose? verbose?)) + '())))) + +(define (oci-configuration->shepherd-services config) + (let* ((runtime (oci-configuration-runtime config)) + (system-runtime-cli + (oci-runtime-system-cli config)) + (home-runtime-cli + (oci-runtime-home-cli config)) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (user (oci-configuration-user config)) + (group + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + (oci-runtime-group config (oci-configuration-group config)))) + (verbose? (oci-configuration-verbose? config))) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group group + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking)))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (delete-duplicate-ranges ranges) + (delete-duplicates ranges + (lambda args + (apply string=? (map subid-range-name ranges))))) + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + (define subgids (oci-configuration-subgids-range config)) + (define subuids (oci-configuration-subuids-range config)) + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (delete-duplicate-ranges + (cons + (if (not (maybe-value-set? subgids)) + (subid-range (name user)) + subgids) + container-users))) + (define subuid-ranges + (delete-duplicate-ranges + (cons + (if (not (maybe-value-set? subuids)) + (subid-range (name user)) + subuids) + container-users))) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. ~as names should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + `(,bash-minimal + ,@(if (string? runtime-cli) + '() + (list + (cond + ((maybe-value-set? runtime-cli) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))))) + +(define (oci-configuration-extend config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name)))) + +(define oci-service-type + (service-type (name 'oci) + (extensions + (list + (service-extension profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension subids-service-type + oci-service-subids) + (service-extension account-service-type + oci-service-accounts) + (service-extension shepherd-root-service-type + oci-configuration->shepherd-services))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend oci-configuration-extend) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI object such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 828ceea313a..125e748bb0e 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; for backwards compatibility, until the + ;; oci-container-service-type is fully deprecated + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -297,17 +302,25 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; for backwards compatibility, until the +;; oci-container-service-type is fully deprecated +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..5e6f39387e7 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -345,3 +350,995 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #: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)) + #~(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-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "rootless-podman services started successfully" + (begin + (define (run-test) + (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)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Services didn't come up after more than 60 seconds") + (if (equal? '("cgroup" "cgroup") + (run-test)) + #t + (begin + (sleep 1) + (format #t "Services didn't come up yet, retrying with attempt ~a~%" + (+ 1 attempts)) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (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 6)) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "network" "ls" "-n" "--format" "\"{{.Name}}\"" + "|" "tr" "' '" "'\n'"))) + + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + + (stable-sort + (slurp "cat" (string-append ,out-dir "/response")) + string<=?)) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-network" "podman") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response "\n") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? (list "value") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + '("hello") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "write to volumes" + '("world") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-equal "can read ports over network" + '("out of office") + (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 "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let ((response (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))'"))) + (call-with-output-file (string-append ,out-dir "/response") + (lambda (port) + (display (string-join response " ") port))))) + (lambda () + (primitive-exit 127)))) + (pid + (cdr (waitpid pid)))) + + (wait-for-file (string-append ,out-dir "/response")) + (slurp "cat" (string-append ,out-dir "/response"))) + marionette)) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list (oci-network-configuration (name "my-network")))) + (volumes + (list (oci-volume-configuration (name "my-volume")))) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #: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)) + #~(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)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "volume" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("my-volume") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (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 + (list "sh" "-l" "-c" + (string-join + args + " ")))) + (output (read-lines port)) + (status (close-pipe port))) + output))) + + (stable-sort + (slurp + "/run/current-system/profile/bin/docker" + "network" "ls" "--format" "\"{{.Name}}\"") + string<=?)) + + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 80) + (error "Service didn't come up after more than 80 seconds") + (if (equal? '("bridge" "host" "my-network" "none") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-assert "first container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'first #:timeout 120) + #t) + marionette)) + + (test-assert "second container running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'second #:timeout 120) + #t) + marionette)) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + marionette)) + ;; Allow image to be loaded on slower machines + (let loop ((attempts 0)) + (if (= attempts 180) + (error "Service didn't come up after more than 180 seconds") + (if (equal? "value" + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) + + (test-equal "mounting host files" + "hello" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))")) + marionette)) + + (test-equal "write to volumes" + "world" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) + marionette)) + + (test-equal "can read ports over network" + "out of office" + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "(begin (use-modules (web client)) +(define-values (response out) + (http-get \"http://first:8080\")) +(display out))")) + marionette)) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:59:02 GMT) Full text and rfc822 format available.Message #167 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v10 1/7] services: rootless-podman: Use login shell. Date: Mon, 5 May 2025 09:57:48 +0200
This commit allows for having PATH set when changing the owner of /sys/fs/group. * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 --- gnu/services/containers.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index b3cd109ce6c..d5a211765a6 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) (rootless-podman-configuration-group-name config)) (program-file "cgroups2-fs-owner-entrypoint" #~(system* - (string-append #+bash-minimal "/bin/bash") "-c" + (string-append #+bash-minimal "/bin/bash") "-l" "-c" (string-append "echo Setting /sys/fs/cgroup " "group ownership to " #$group " && chown -v " "root:" #$group " /sys/fs/cgroup && " base-commit: 63088c295d81cc3d0e808c478d4fe479a2c90102 -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:59:03 GMT) Full text and rfc822 format available.Message #170 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v10 3/7] tests: oci-container: Set explicit timeouts. Date: Mon, 5 May 2025 09:57:50 +0200
* gnu/tests/docker.scm: Simplify %test-oci-container test case and add explicit timeouts to tests outcomes. --- gnu/tests/docker.scm | 99 ++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:59:04 GMT) Full text and rfc822 format available.Message #173 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v10 5/7] tests: Use lower-oci-image-state in container tests. Date: Mon, 5 May 2025 09:57:52 +0200
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/services/containers.scm: Export lower-oci-image-state. * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a --- gnu/services/containers.scm | 2 + gnu/tests/containers.scm | 80 ++++++++++++++--------------- gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- 3 files changed, 95 insertions(+), 87 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 66b46456800..a8d10d842da 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -74,6 +74,8 @@ (define-module (gnu services containers) oci-image-system oci-image-grafts? + lower-oci-image-state + oci-container-configuration oci-container-configuration? oci-container-configuration-fields diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 5e6f39387e7..8cdd86e7ae3 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -69,13 +69,47 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-rootless-podman-os diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..07edd9d5341 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -57,6 +58,40 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +208,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +217,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +349,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.49.0
andrew <at> trop.in, gabriel <at> erlikon.ch, hako <at> ultrarare.space, janneke <at> gnu.org, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, tanguy <at> bioneland.org, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:59:04 GMT) Full text and rfc822 format available.Message #176 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v10 7/7] home: Add home-oci-service-type. Date: Mon, 5 May 2025 09:57:54 +0200
* gnu/home/service/containers.scm: New file; * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (OCI backed services): Document it. Change-Id: I8ce5b301e8032d0a7b2a9ca46752738cdee1f030 --- doc/guix.texi | 114 +++++++++++++++++++++++++++++++ gnu/home/services/containers.scm | 50 ++++++++++++++ gnu/local.mk | 1 + 3 files changed, 165 insertions(+) create mode 100644 gnu/home/services/containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index de0d7ccfd5a..3f4da28af72 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -51429,6 +51429,120 @@ Miscellaneous Home Services (dicod-configuration @dots{}))) @end lisp +@subsubheading OCI backed services + +@cindex OCI-backed, for Home +The @code{(gnu home services containers)} module provides the following service: + +@defvar home-oci-service-type +This is the type of the service that allows to manage your OCI containers with +the same consistent interface you use for your other Home Shepherd services. +@end defvar + +This service is a direct mapping of the @code{oci-service-type} system +service (@pxref{Miscellaneous Services, OCI backed services}). You can +use it like this: + +@lisp +(use-modules (gnu services containers) + (gnu home services containers)) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) + +@end lisp + +You may specify a custom configuration by providing a +@code{oci-configuration} record, exactly like for +@code{oci-service-type}, but wrapping it in @code{for-home}: + +@lisp +(use-modules (gnu services) + (gnu services containers) + (gnu home services containers)) + +(service home-oci-service-type + (for-home + (oci-configuration + (runtime 'podman) + (verbose? #t)))) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) +@end lisp + @node Invoking guix home @section Invoking @command{guix home} diff --git a/gnu/home/services/containers.scm b/gnu/home/services/containers.scm new file mode 100644 index 00000000000..938dde2f37a --- /dev/null +++ b/gnu/home/services/containers.scm @@ -0,0 +1,50 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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 home services containers) + #:use-module (gnu home services) + #:use-module (gnu home services shepherd) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services containers) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (home-oci-service-type)) + +(define home-oci-service-type + (service-type (inherit (system->home-service-type oci-service-type)) + (extensions + (list + (service-extension home-profile-service-type + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) + (service-extension home-shepherd-service-type + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) + (extend + (lambda (config extension) + (for-home + (oci-configuration + (inherit (oci-configuration-extend config extension)))))) + (default-value (for-home (oci-configuration))))) diff --git a/gnu/local.mk b/gnu/local.mk index e25fcc115be..1be38d6e217 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -103,6 +103,7 @@ GNU_SYSTEM_MODULES = \ %D%/home.scm \ %D%/home/services.scm \ %D%/home/services/admin.scm \ + %D%/home/services/containers.scm \ %D%/home/services/desktop.scm \ %D%/home/services/dict.scm \ %D%/home/services/dotfiles.scm \ -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:59:05 GMT) Full text and rfc822 format available.Message #179 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v10 2/7] services: oci-container-configuration: Move to (gnu services containers). Date: Mon, 5 May 2025 09:57:49 +0200
This patch moves the oci-container-configuration and related configuration records to (gnu services containers). Public symbols are still exported for backwards compatibility but since the oci-container-service-type will be deprecated in favor of the more general oci-service-type, everything is moved outside of the docker related module. * gnu/services/docker.scm: Move everything related to oci-container-configuration to... * gnu/services/containers.scm: ...here.scm. Change-Id: Iae599dd5cc7442eb632f0c1b3b12f6b928397ae7 --- gnu/services/containers.scm | 549 +++++++++++++++++++++++++++++++++- gnu/services/docker.scm | 577 +++--------------------------------- 2 files changed, 584 insertions(+), 542 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index d5a211765a6..24f31c756b8 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -1,5 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,19 +17,31 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services containers) + #:use-module (gnu image) + #:use-module (gnu packages admin) #:use-module (gnu packages bash) #:use-module (gnu packages containers) + #:use-module (gnu packages docker) #: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) #:use-module (gnu system accounts) + #:use-module (gnu system image) #:use-module (gnu system shadow) #:use-module (gnu system pam) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) + #:use-module (guix monads) #:use-module (guix packages) + #:use-module (guix profiles) + #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? rootless-podman-configuration-fields @@ -48,7 +60,44 @@ (define-module (gnu services containers) rootless-podman-shepherd-services rootless-podman-service-etc - rootless-podman-service-type)) + rootless-podman-service-type + + oci-image + oci-image? + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + + oci-container-shepherd-service + %oci-container-accounts)) (define (gexp-or-string? value) (or (gexp? value) @@ -190,7 +239,7 @@ (define (rootless-podman-cgroups-limits-service config) rootless-podman-shared-root-fs)) (one-shot? #t) (documentation - "Allow setting cgroups limits: cpu, cpuset, memory and + "Allow setting cgroups limits: cpu, cpuset, io, memory and pids.") (start #~(make-forkexec-constructor @@ -244,3 +293,497 @@ (define rootless-podman-service-type (default-value (rootless-podman-configuration)) (description "This service configures rootless @code{podman} on the Guix System."))) + + +;;; +;;; OCI container. +;;; + +(define (oci-sanitize-pair pair delimiter) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$delimiter #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define (oci-sanitize-mixed-list name value delimiter) + (map + (lambda (el) + (cond ((string? el) el) + ((pair? el) (oci-sanitize-pair el delimiter)) + (else + (raise + (formatted-message + (G_ "~a members must be either a string or a pair but ~a was +found!") + name el))))) + value)) + +(define (oci-sanitize-host-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "host-environment" value "=")) + +(define (oci-sanitize-environment value) + ;; Expected spec format: + ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") + (oci-sanitize-mixed-list "environment" value "=")) + +(define (oci-sanitize-ports value) + ;; Expected spec format: + ;; '(("8088" . "80") "2022:22") + (oci-sanitize-mixed-list "ports" value ":")) + +(define (oci-sanitize-volumes value) + ;; Expected spec format: + ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") + (oci-sanitize-mixed-list "volumes" value ":")) + +(define (oci-sanitize-shepherd-actions value) + (map + (lambda (el) + (if (shepherd-action? el) + el + (raise + (formatted-message + (G_ "shepherd-actions may only be shepherd-action records +but ~a was found") el)))) + value)) + +(define (oci-sanitize-extra-arguments value) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (map + (lambda (el) + (if (valid? el) + el + (raise + (formatted-message + (G_ "extra arguments may only be strings, gexps or file-like objects +but ~a was found") el)))) + value)) + +(define (oci-image-reference image) + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + +(define (oci-lowerable-image? image) + (or (manifest? image) + (operating-system? image) + (gexp? image) + (file-like? image))) + +(define (string-or-oci-image? image) + (or (string? image) + (oci-image? image))) + +(define list-of-symbols? + (list-of symbol?)) + +(define-maybe/no-serialization string) + +(define-configuration/no-serialization oci-image + (repository + (string) + "A string like @code{myregistry.local:5000/testing/test-image} that names +the OCI image.") + (tag + (string "latest") + "A string representing the OCI image tag. Defaults to @code{latest}.") + (value + (oci-lowerable-image) + "A @code{manifest} or @code{operating-system} record that will be lowered +into an OCI compatible tarball. Otherwise this field's value can be a gexp +or a file-like object that evaluates to an OCI compatible tarball.") + (pack-options + (list '()) + "An optional set of keyword arguments that will be passed to the +@code{docker-image} procedure from @code{guix scripts pack}. They can be used +to replicate @command{guix pack} behavior: + +@lisp +(oci-image + (repository \"guile\") + (tag \"3\") + (manifest (specifications->manifest '(\"guile\"))) + (pack-options + '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) + #:max-layers 2))) +@end lisp + +If the @code{value} field is an @code{operating-system} record, this field's +value will be ignored.") + (system + (maybe-string) + "Attempt to build for a given system, e.g. \"i686-linux\"") + (target + (maybe-string) + "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") + (grafts? + (boolean #f) + "Whether to allow grafting or not in the pack build.")) + +(define-configuration/no-serialization oci-container-configuration + (user + (string "oci-container") + "The user under whose authority docker commands will be run.") + (group + (string "docker") + "The group under whose authority docker commands will be run.") + (command + (list-of-strings '()) + "Overwrite the default command (@code{CMD}) of the image.") + (entrypoint + (maybe-string) + "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") + (host-environment + (list '()) + "Set environment variables in the host environment where @command{docker run} +is invoked. This is especially useful to pass secrets from the host to the +container without having them on the @command{docker run}'s command line: by +setting the @code{MYSQL_PASSWORD} on the host and by passing +@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is +possible to securely set values in the container environment. This field's +value can be a list of pairs or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to @code{make-forkexec-constructor}." + (sanitizer oci-sanitize-host-environment)) + (environment + (list '()) + "Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: + +@lisp +(list '(\"LANGUAGE\" . \"eo:ca:eu\") + \"JAVA_HOME=/opt/java\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} +documentation for semantics." + (sanitizer oci-sanitize-environment)) + (image + (string-or-oci-image) + "The image used to build the container. It can be a string or an +@code{oci-image} record. Strings are resolved by the Docker +Engine, and follow the usual format +@code{myregistry.local:5000/testing/test-image:tag}.") + (provision + (maybe-string) + "Set the name of the provisioned Shepherd service.") + (requirement + (list-of-symbols '()) + "Set additional Shepherd services dependencies to the provisioned Shepherd +service.") + (log-file + (maybe-string) + "When @code{log-file} is set, it names the file to which the service’s +standard output and standard error are redirected. @code{log-file} is created +if it does not exist, otherwise it is appended to.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (respawn? + (boolean #f) + "Whether to restart the service when it stops, for instance when the +underlying process dies.") + (shepherd-actions + (list '()) + "This is a list of @code{shepherd-action} records defining actions supported +by the service." + (sanitizer oci-sanitize-shepherd-actions)) + (network + (maybe-string) + "Set a Docker network for the spawned container.") + (ports + (list '()) + "Set the port or port ranges to expose from the spawned container. This can +be a list of pairs or strings, even mixed: + +@lisp +(list '(\"8080\" . \"80\") + \"10443:443\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} +documentation for semantics." + (sanitizer oci-sanitize-ports)) + (volumes + (list '()) + "Set volume mappings for the spawned container. This can be a +list of pairs or strings, even mixed: + +@lisp +(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") + \"/gnu/store:/gnu/store\") +@end lisp + +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the Docker CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} +documentation for semantics." + (sanitizer oci-sanitize-volumes)) + (container-user + (maybe-string) + "Set the current user inside the spawned container. You can refer to the +@url{https://docs.docker.com/engine/reference/run/#user,upstream} +documentation for semantics.") + (workdir + (maybe-string) + "Set the current working for the spawned Shepherd service. +You can refer to the +@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} +documentation for semantics.") + (extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker run} invokation." + (sanitizer oci-sanitize-extra-arguments))) + +(define oci-container-configuration->options + (lambda (config) + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config)))))))) + +(define* (get-keyword-value args keyword #:key (default #f)) + (let ((kv (memq keyword args))) + (if (and kv (>= (length kv) 2)) + (cadr kv) + default))) + +(define (lower-operating-system os target system) + (mlet* %store-monad + ((tarball + (lower-object + (system-image (os->image os #:type docker-image-type)) + system + #:target target))) + (return tarball))) + +(define (lower-manifest name image target system) + (define value (oci-image-value image)) + (define options (oci-image-pack-options image)) + (define image-reference + (oci-image-reference image)) + (define image-tag + (let* ((extra-options + (get-keyword-value options #:extra-options)) + (image-tag-option + (and extra-options + (get-keyword-value extra-options #:image-tag)))) + (if image-tag-option + '() + `(#:extra-options (#:image-tag ,image-reference))))) + + (mlet* %store-monad + ((_ (set-grafting + (oci-image-grafts? image))) + (guile (set-guile-for-build (default-guile))) + (profile + (profile-derivation value + #:target target + #:system system + #:hooks '() + #:locales? #f)) + (tarball (apply pack:docker-image + `(,name ,profile + ,@options + ,@image-tag + #:localstatedir? #t)))) + (return tarball))) + +(define (lower-oci-image name image) + (define value (oci-image-value image)) + (define image-target (oci-image-target image)) + (define image-system (oci-image-system image)) + (define target + (if (maybe-value-set? image-target) + image-target + (%current-target-system))) + (define system + (if (maybe-value-set? image-system) + image-system + (%current-system))) + (with-store store + (run-with-store store + (match value + ((? manifest? value) + (lower-manifest name image target system)) + ((? operating-system? value) + (lower-operating-system value target system)) + ((or (? gexp? value) + (? file-like? value)) + value) + (_ + (raise + (formatted-message + (G_ "oci-image value must contain only manifest, +operating-system, gexp or file-like records but ~a was found") + value)))) + #:target target + #:system system))) + +(define (%oci-image-loader name image tag) + (let ((docker (file-append docker-cli "/bin/docker")) + (tarball (lower-oci-image name image))) + (with-imported-modules '((guix build utils)) + (program-file (format #f "~a-image-loader" name) + #~(begin + (use-modules (guix build utils) + (ice-9 popen) + (ice-9 rdelim)) + + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) + (define line + (read-line + (open-input-pipe + (string-append #$docker " load -i " #$tarball)))) + + (unless (or (eof-object? line) + (string-null? line)) + (format #t "~a~%" line) + (let ((repository&tag + (string-drop line + (string-length + "Loaded image: ")))) + + (invoke #$docker "tag" repository&tag #$tag) + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) + +(define (oci-container-shepherd-service config) + (define (guess-name name image) + (if (maybe-value-set? name) + name + (string-append "docker-" + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))))) + + (let* ((docker (file-append docker-cli "/bin/docker")) + (actions (oci-container-configuration-shepherd-actions config)) + (auto-start? + (oci-container-configuration-auto-start? config)) + (user (oci-container-configuration-user config)) + (group (oci-container-configuration-group config)) + (host-environment + (oci-container-configuration-host-environment config)) + (command (oci-container-configuration-command config)) + (log-file (oci-container-configuration-log-file config)) + (provision (oci-container-configuration-provision config)) + (requirement (oci-container-configuration-requirement config)) + (respawn? + (oci-container-configuration-respawn? config)) + (image (oci-container-configuration-image config)) + (image-reference (oci-image-reference image)) + (options (oci-container-configuration->options config)) + (name (guess-name provision image)) + (extra-arguments + (oci-container-configuration-extra-arguments config))) + + (shepherd-service (provision `(,(string->symbol name))) + (requirement `(dockerd user-processes ,@requirement)) + (respawn? respawn?) + (auto-start? auto-start?) + (documentation + (string-append + "Docker backed Shepherd service for " + (if (oci-image? image) name image) ".")) + (start + #~(lambda () + #$@(if (oci-image? image) + #~((invoke #$(%oci-image-loader + name image image-reference))) + #~()) + (fork+exec-command + ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] + (list #$docker "run" "--rm" "--name" #$name + #$@options #$@extra-arguments + #$image-reference #$@command) + #:user #$user + #:group #$group + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #:environment-variables + (list #$@host-environment)))) + (stop + #~(lambda _ + (invoke #$docker "rm" "-f" #$name))) + (actions + (if (oci-image? image) + '() + (append + (list + (shepherd-action + (name 'pull) + (documentation + (format #f "Pull ~a's image (~a)." + name image)) + (procedure + #~(lambda _ + (invoke #$docker "pull" #$image))))) + actions)))))) + +(define %oci-container-accounts + (list (user-account + (name "oci-container") + (comment "OCI services account") + (group "docker") + (system? #t) + (home-directory "/var/empty") + (shell (file-append shadow "/sbin/nologin"))))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 9ab3e583345..828ceea313a 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -5,7 +5,7 @@ ;;; Copyright © 2020 Efraim Flashner <efraim <at> flashner.co.il> ;;; Copyright © 2020 Jesse Dowell <jessedowell <at> gmail.com> ;;; Copyright © 2021 Brice Waegeneire <brice <at> waegenei.re> -;;; Copyright © 2023, 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2023, 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,72 +23,60 @@ ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. (define-module (gnu services docker) - #:use-module (gnu image) #:use-module (gnu services) #:use-module (gnu services configuration) - #:use-module (gnu services base) - #:use-module (gnu services dbus) + #:use-module (gnu services containers) #:use-module (gnu services shepherd) - #:use-module (gnu system) - #:use-module (gnu system image) #:use-module (gnu system privilege) #:use-module (gnu system shadow) - #:use-module (gnu packages admin) ;shadow #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity - #:use-module (guix records) - #:use-module (guix diagnostics) #:use-module (guix gexp) - #:use-module (guix i18n) - #:use-module (guix monads) - #:use-module (guix packages) - #:use-module (guix profiles) - #:use-module ((guix scripts pack) #:prefix pack:) - #:use-module (guix store) + #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:re-export (oci-image ;for backwards compatibility, until the + oci-image? ;oci-container-service-type is fully deprecated + oci-image-fields + oci-image-repository + oci-image-tag + oci-image-value + oci-image-pack-options + oci-image-target + oci-image-system + oci-image-grafts? + oci-container-configuration + oci-container-configuration? + oci-container-configuration-fields + oci-container-configuration-user + oci-container-configuration-group + oci-container-configuration-command + oci-container-configuration-entrypoint + oci-container-configuration-host-environment + oci-container-configuration-environment + oci-container-configuration-image + oci-container-configuration-provision + oci-container-configuration-requirement + oci-container-configuration-log-file + oci-container-configuration-auto-start? + oci-container-configuration-respawn? + oci-container-configuration-shepherd-actions + oci-container-configuration-network + oci-container-configuration-ports + oci-container-configuration-volumes + oci-container-configuration-container-user + oci-container-configuration-workdir + oci-container-configuration-extra-arguments + oci-container-shepherd-service + %oci-container-accounts) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-image - oci-image? - oci-image-fields - oci-image-repository - oci-image-tag - oci-image-value - oci-image-pack-options - oci-image-target - oci-image-system - oci-image-grafts? - oci-container-configuration - oci-container-configuration? - oci-container-configuration-fields - oci-container-configuration-user - oci-container-configuration-group - oci-container-configuration-command - oci-container-configuration-entrypoint - oci-container-configuration-host-environment - oci-container-configuration-environment - oci-container-configuration-image - oci-container-configuration-provision - oci-container-configuration-requirement - oci-container-configuration-log-file - oci-container-configuration-auto-start? - oci-container-configuration-respawn? - oci-container-configuration-shepherd-actions - oci-container-configuration-network - oci-container-configuration-ports - oci-container-configuration-volumes - oci-container-configuration-container-user - oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-service-type - oci-container-shepherd-service - %oci-container-accounts)) + oci-container-service-type)) (define-maybe file-like) @@ -309,495 +297,6 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (oci-sanitize-pair pair delimiter) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (match pair - (((? valid? key) . (? valid? value)) - #~(string-append #$key #$delimiter #$value)) - (_ - (raise - (formatted-message - (G_ "pair members must contain only strings, gexps or file-like objects -but ~a was found") - pair))))) - -(define (oci-sanitize-mixed-list name value delimiter) - (map - (lambda (el) - (cond ((string? el) el) - ((pair? el) (oci-sanitize-pair el delimiter)) - (else - (raise - (formatted-message - (G_ "~a members must be either a string or a pair but ~a was -found!") - name el))))) - value)) - -(define (oci-sanitize-host-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "host-environment" value "=")) - -(define (oci-sanitize-environment value) - ;; Expected spec format: - ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") - (oci-sanitize-mixed-list "environment" value "=")) - -(define (oci-sanitize-ports value) - ;; Expected spec format: - ;; '(("8088" . "80") "2022:22") - (oci-sanitize-mixed-list "ports" value ":")) - -(define (oci-sanitize-volumes value) - ;; Expected spec format: - ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") - (oci-sanitize-mixed-list "volumes" value ":")) - -(define (oci-sanitize-shepherd-actions value) - (map - (lambda (el) - (if (shepherd-action? el) - el - (raise - (formatted-message - (G_ "shepherd-actions may only be shepherd-action records -but ~a was found") el)))) - value)) - -(define (oci-sanitize-extra-arguments value) - (define (valid? member) - (or (string? member) - (gexp? member) - (file-like? member))) - (map - (lambda (el) - (if (valid? el) - el - (raise - (formatted-message - (G_ "extra arguments may only be strings, gexps or file-like objects -but ~a was found") el)))) - value)) - -(define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) - -(define (oci-lowerable-image? image) - (or (manifest? image) - (operating-system? image) - (gexp? image) - (file-like? image))) - -(define (string-or-oci-image? image) - (or (string? image) - (oci-image? image))) - -(define list-of-symbols? - (list-of symbol?)) - -(define-maybe/no-serialization string) - -(define-configuration/no-serialization oci-image - (repository - (string) - "A string like @code{myregistry.local:5000/testing/test-image} that names -the OCI image.") - (tag - (string "latest") - "A string representing the OCI image tag. Defaults to @code{latest}.") - (value - (oci-lowerable-image) - "A @code{manifest} or @code{operating-system} record that will be lowered -into an OCI compatible tarball. Otherwise this field's value can be a gexp -or a file-like object that evaluates to an OCI compatible tarball.") - (pack-options - (list '()) - "An optional set of keyword arguments that will be passed to the -@code{docker-image} procedure from @code{guix scripts pack}. They can be used -to replicate @command{guix pack} behavior: - -@lisp -(oci-image - (repository \"guile\") - (tag \"3\") - (manifest (specifications->manifest '(\"guile\"))) - (pack-options - '(#:symlinks ((\"/bin/guile\" -> \"bin/guile\")) - #:max-layers 2))) -@end lisp - -If the @code{value} field is an @code{operating-system} record, this field's -value will be ignored.") - (system - (maybe-string) - "Attempt to build for a given system, e.g. \"i686-linux\"") - (target - (maybe-string) - "Attempt to cross-build for a given triple, e.g. \"aarch64-linux-gnu\"") - (grafts? - (boolean #f) - "Whether to allow grafting or not in the pack build.")) - -(define-configuration/no-serialization oci-container-configuration - (user - (string "oci-container") - "The user under whose authority docker commands will be run.") - (group - (string "docker") - "The group under whose authority docker commands will be run.") - (command - (list-of-strings '()) - "Overwrite the default command (@code{CMD}) of the image.") - (entrypoint - (maybe-string) - "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") - (host-environment - (list '()) - "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing -@code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is -possible to securely set values in the container environment. This field's -value can be a list of pairs or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to @code{make-forkexec-constructor}." - (sanitizer oci-sanitize-host-environment)) - (environment - (list '()) - "Set environment variables inside the container. This can be a list of pairs -or strings, even mixed: - -@lisp -(list '(\"LANGUAGE\" . \"eo:ca:eu\") - \"JAVA_HOME=/opt/java\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." - (sanitizer oci-sanitize-environment)) - (image - (string-or-oci-image) - "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format -@code{myregistry.local:5000/testing/test-image:tag}.") - (provision - (maybe-string) - "Set the name of the provisioned Shepherd service.") - (requirement - (list-of-symbols '()) - "Set additional Shepherd services dependencies to the provisioned Shepherd -service.") - (log-file - (maybe-string) - "When @code{log-file} is set, it names the file to which the service’s -standard output and standard error are redirected. @code{log-file} is created -if it does not exist, otherwise it is appended to.") - (auto-start? - (boolean #t) - "Whether this service should be started automatically by the Shepherd. If it -is @code{#f} the service has to be started manually with @command{herd start}.") - (respawn? - (boolean #f) - "Whether to restart the service when it stops, for instance when the -underlying process dies.") - (shepherd-actions - (list '()) - "This is a list of @code{shepherd-action} records defining actions supported -by the service." - (sanitizer oci-sanitize-shepherd-actions)) - (network - (maybe-string) - "Set a Docker network for the spawned container.") - (ports - (list '()) - "Set the port or port ranges to expose from the spawned container. This can -be a list of pairs or strings, even mixed: - -@lisp -(list '(\"8080\" . \"80\") - \"10443:443\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." - (sanitizer oci-sanitize-ports)) - (volumes - (list '()) - "Set volume mappings for the spawned container. This can be a -list of pairs or strings, even mixed: - -@lisp -(list '(\"/root/data/grafana\" . \"/var/lib/grafana\") - \"/gnu/store:/gnu/store\") -@end lisp - -Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." - (sanitizer oci-sanitize-volumes)) - (container-user - (maybe-string) - "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") - (workdir - (maybe-string) - "Set the current working for the spawned Shepherd service. -You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") - (extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invocation." - (sanitizer oci-sanitize-extra-arguments))) - -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) - -(define (lower-operating-system os target system) - (mlet* %store-monad - ((tarball - (lower-object - (system-image (os->image os #:type docker-image-type)) - system - #:target target))) - (return tarball))) - -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - - (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) - (guile (set-guile-for-build (default-guile))) - (profile - (profile-derivation value - #:target target - #:system system - #:hooks '() - #:locales? #f)) - (tarball (apply pack:docker-image - `(,name ,profile - ,@options - ,@image-tag - #:localstatedir? #t)))) - (return tarball))) - -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) - (define target - (if (maybe-value-set? image-target) - image-target - (%current-target-system))) - (define system - (if (maybe-value-set? image-system) - image-system - (%current-system))) - (with-store store - (run-with-store store - (match value - ((? manifest? value) - (lower-manifest name image target system)) - ((? operating-system? value) - (lower-operating-system value target system)) - ((or (? gexp? value) - (? file-like? value)) - value) - (_ - (raise - (formatted-message - (G_ "oci-image value must contain only manifest, -operating-system, gexp or file-like records but ~a was found") - value)))) - #:target target - #:system system))) - -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) - #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) - (respawn? respawn?) - (auto-start? auto-start?) - (documentation - (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) - (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) - (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) - (actions - (if (oci-image? image) - '() - (append - (list - (shepherd-action - (name 'pull) - (documentation - (format #f "Pull ~a's image (~a)." - name image)) - (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts - (list (user-account - (name "oci-container") - (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") - (shell (file-append shadow "/sbin/nologin"))))) - (define (configs->shepherd-services configs) (map oci-container-shepherd-service configs)) -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Mon, 05 May 2025 07:59:05 GMT) Full text and rfc822 format available.Message #182 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v10 6/7] services: oci: Migrate oci-configuration to (guix records). Date: Mon, 5 May 2025 09:57:53 +0200
This commit migrates oci-configuration to (guix records) singe it appears (for-home (oci-configuration ...)) does not work as expected with (gnu services configuration). This is supposed to be completely transparent for users and can be reverted in the future once this has been implemented. * gnu/service/containers.scm: Migrate oci-configuration to (guix records). --- gnu/services/containers.scm | 199 +++++++++++++++++++++--------------- 1 file changed, 117 insertions(+), 82 deletions(-) diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index a8d10d842da..a974227e164 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -39,6 +39,7 @@ (define-module (gnu services containers) #:use-module (guix packages) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix records) #:use-module (guix store) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -168,6 +169,7 @@ (define-module (gnu services containers) oci-container-shepherd-service oci-objects-merge-lst oci-extension-merge + oci-service-extension-wrap-validate oci-service-type oci-service-accounts oci-service-profile @@ -395,7 +397,7 @@ (define (oci-runtime-name runtime) (define (oci-runtime-group runtime maybe-group) "Implement the logic behind selection of the group that is to be used by Shepherd to execute OCI commands." - (if (not (maybe-value-set? maybe-group)) + (if (eq? maybe-group #f) (if (eq? 'podman runtime) "cgroup" "docker") @@ -766,62 +768,74 @@ (define (list-of-oci-networks? value) (define (package-or-string? value) (or (package? value) (string? value))) -(define-maybe/no-serialization package-or-string) - -(define-configuration/no-serialization oci-configuration - (runtime - (symbol 'docker) - "The OCI runtime to use to run commands. It can be either @code{'docker} or -@code{'podman}." - (sanitizer oci-sanitize-runtime)) - (runtime-cli - (maybe-package-or-string) - "The OCI runtime command line to be installed in the system profile and used -to provision OCI resources, it can be either a package or a string representing -an absolute path to the runtime binary entrypoint. When unset it will default -to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} -package for the @code{'podman} runtime.") - (runtime-extra-arguments - (list '()) - "A list of strings, gexps or file-like objects that will be placed -after each @command{docker} or @command{podman} invokation.") - (user - (string "oci-container") - "The user name under whose authority OCI runtime commands will be run.") - (group - (maybe-string) - "The group name under whose authority OCI commands will be run. When -using the @code{'podman} OCI runtime, this field will be ignored and the -default group of the user configured in the @code{user} field will be used.") - (subuids-range - (maybe-subid-range) - "An optional @code{subid-range} record allocating subuids for the user from -the @code{user} field. When unset, with the rootless Podman OCI runtime, it -defaults to @code{(subid-range (name \"oci-container\"))}.") - (subgids-range - (maybe-subid-range) - "An optional @code{subid-range} record allocating subgids for the user from -the @code{user} field. When unset, with the rootless Podman OCI runtime, it -defaults to @code{(subid-range (name \"oci-container\"))}.") - (containers - (list-of-oci-containers '()) - "The list of @code{oci-container-configuration} records representing the -containers to provision. Most users are supposed not to use this field and use -the @code{oci-extension} record instead.") - (networks - (list-of-oci-networks '()) - "The list of @code{oci-network-configuration} records representing the -networks to provision. Most users are supposed not to use this field and use -the @code{oci-extension} record instead.") - (volumes - (list-of-oci-volumes '()) - "The list of @code{oci-volume-configuration} records representing the -volumes to provision. Most users are supposed not to use this field and use -the @code{oci-extension} record instead.") - (verbose? - (boolean #f) - "When true, additional output will be printed, allowing to better follow the -flow of execution.")) +;; (for-home (oci-configuration ...)) is not able to replace for-home? with #t, +;; pk prints #f. Once for-home will be able to work with (gnu services configuration) the +;; record can be migrated back to define-configuration. +(define-record-type* <oci-configuration> + oci-configuration + make-oci-configuration + oci-configuration? + this-oci-configuration + + (runtime oci-configuration-runtime + (default 'docker)) + (runtime-cli oci-configuration-runtime-cli + (default #f)) ; package or string + (runtime-extra-arguments oci-configuration-runtime-extra-arguments ; strings or gexps + (default '())) ; or file-like objects + (user oci-configuration-user + (default "oci-container")) + (group oci-configuration-group ; string + (default #f)) + (subuids-range oci-configuration-subuids-range ; subid-range + (default #f)) + (subgids-range oci-configuration-subgids-range ; subid-range + (default #f)) + (containers oci-configuration-containers ; oci-container-configurations + (default '())) + (networks oci-configuration-networks ; oci-network-configurations + (default '())) + (volumes oci-configuration-volumes ; oci-volume-configurations + (default '())) + (verbose? oci-configuration-verbose? + (default #f)) + (home-service? oci-configuration-home-service? + (default for-home?) (innate))) + +;; TODO: This procedure can be dropped once we switch to define-configuration for +;; oci-configuration. +(define (oci-configuration-valid? config) + (define runtime-cli + (oci-configuration-runtime-cli config)) + (define group + (oci-configuration-group config)) + (define subuids-range + (oci-configuration-subuids-range config)) + (define subgids-range + (oci-configuration-subgids-range config)) + (and + (symbol? + (oci-sanitize-runtime (oci-configuration-runtime config))) + (or (eq? runtime-cli #f) + (package-or-string? runtime-cli)) + (list? (oci-configuration-runtime-extra-arguments config)) + (string? (oci-configuration-user config)) + (or (eq? group #f) + (string? group)) + (or (eq? subuids-range #f) + (subid-range? subuids-range)) + (or (eq? subgids-range #f) + (subid-range? subgids-range)) + (list-of-oci-containers? + (oci-configuration-containers config)) + (list-of-oci-networks? + (oci-configuration-networks config)) + (list-of-oci-volumes? + (oci-configuration-volumes config)) + (boolean? + (oci-configuration-verbose? config)) + (boolean? + (oci-configuration-home-service? config)))) (define (oci-runtime-system-environment runtime user) (if (eq? runtime 'podman) @@ -837,7 +851,7 @@ (define (oci-runtime-cli runtime runtime-cli path) ;; It is a user defined absolute path runtime-cli #~(string-append - #$(if (not (maybe-value-set? runtime-cli)) + #$(if (eq? runtime-cli #f) path runtime-cli) #$(if (eq? 'podman runtime) @@ -1581,18 +1595,27 @@ (define (oci-configuration->shepherd-services config) (passwd:gid (getpwnam #$user)))) (oci-runtime-group config (oci-configuration-group config)))) - (verbose? (oci-configuration-verbose? config))) - (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes - #:user user - #:group group - #:verbose? verbose? - #:runtime-extra-arguments - runtime-extra-arguments - #:runtime-environment - (oci-runtime-system-environment runtime user) - #:runtime-requirement - (oci-runtime-system-requirement runtime) - #:networks-requirement '(networking)))) + (verbose? (oci-configuration-verbose? config)) + (home-service? + (oci-configuration-home-service? config))) + (if home-service? + (oci-state->shepherd-services runtime home-runtime-cli containers networks volumes + #:verbose? verbose? + #:networks-name + (oci-networks-home-shepherd-name runtime) + #:volumes-name + (oci-volumes-home-shepherd-name runtime)) + (oci-state->shepherd-services runtime system-runtime-cli containers networks volumes + #:user user + #:group group + #:verbose? verbose? + #:runtime-extra-arguments + runtime-extra-arguments + #:runtime-environment + (oci-runtime-system-environment runtime user) + #:runtime-requirement + (oci-runtime-system-requirement runtime) + #:networks-requirement '(networking))))) (define (oci-service-subids config) "Return a subids-extension record representing subuids and subgids required by @@ -1620,14 +1643,14 @@ (define (oci-service-subids config) (define subgid-ranges (delete-duplicate-ranges (cons - (if (not (maybe-value-set? subgids)) + (if (eq? subgids #f) (subid-range (name user)) subgids) container-users))) (define subuid-ranges (delete-duplicate-ranges (cons - (if (not (maybe-value-set? subuids)) + (if (eq? subuids #f) (subid-range (name user)) subuids) container-users))) @@ -1686,13 +1709,21 @@ (define (oci-service-profile runtime runtime-cli) '() (list (cond - ((maybe-value-set? runtime-cli) + ((not (eq? runtime-cli #f)) runtime-cli) ((eq? 'podman runtime) podman) (else docker-cli)))))) +(define (oci-service-extension-wrap-validate extension) + (lambda (config) + (if (oci-configuration-valid? config) + (extension config) + (raise + (formatted-message + (G_ "Invalide oci-configuration ~a.") config))))) + (define (oci-configuration-extend config extension) (oci-configuration (inherit config) @@ -1721,18 +1752,22 @@ (define oci-service-type (extensions (list (service-extension profile-service-type - (lambda (config) - (let ((runtime-cli - (oci-configuration-runtime-cli config)) - (runtime - (oci-configuration-runtime config))) - (oci-service-profile runtime runtime-cli)))) + (oci-service-extension-wrap-validate + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli))))) (service-extension subids-service-type - oci-service-subids) + (oci-service-extension-wrap-validate + oci-service-subids)) (service-extension account-service-type - oci-service-accounts) + (oci-service-extension-wrap-validate + oci-service-accounts)) (service-extension shepherd-root-service-type - oci-configuration->shepherd-services))) + (oci-service-extension-wrap-validate + oci-configuration->shepherd-services)))) ;; Concatenate OCI object lists. (compose (lambda (args) (fold oci-extension-merge -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Thu, 15 May 2025 04:55:01 GMT) Full text and rfc822 format available.Message #185 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: Giacomo Leidi <goodoldpaul <at> autistici.org> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 1/7] services: rootless-podman: Use login shell. Date: Thu, 15 May 2025 13:53:57 +0900
Hi, Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > This commit allows for having PATH set when changing the owner of > /sys/fs/group. > > * gnu/services/containers.scm (crgroups-fs-owner): Use login shell. > > Change-Id: I9510c637a5332325e05ca5ebc9dfd4de32685c50 > --- > gnu/services/containers.scm | 2 +- > 1 file changed, 1 insertion(+), 1 deletion(-) > > diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm > index b3cd109ce6c..d5a211765a6 100644 > --- a/gnu/services/containers.scm > +++ b/gnu/services/containers.scm > @@ -140,7 +140,7 @@ (define (cgroups-fs-owner-entrypoint config) > (rootless-podman-configuration-group-name config)) > (program-file "cgroups2-fs-owner-entrypoint" > #~(system* > - (string-append #+bash-minimal "/bin/bash") "-c" > + (string-append #+bash-minimal "/bin/bash") "-l" "-c" > (string-append "echo Setting /sys/fs/cgroup " > "group ownership to " #$group " && chown -v " > "root:" #$group " /sys/fs/cgroup && " > > base-commit: 63088c295d81cc3d0e808c478d4fe479a2c90102 I pushed this one. -- Thanks, Maxim
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Thu, 15 May 2025 04:55:02 GMT) Full text and rfc822 format available.Message #188 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: Giacomo Leidi <goodoldpaul <at> autistici.org> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 2/7] services: oci-container-configuration: Move to (gnu services containers). Date: Thu, 15 May 2025 13:54:14 +0900
Hi, Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > This patch moves the oci-container-configuration and related > configuration records to (gnu services containers). > Public symbols are still exported for backwards > compatibility but since the oci-container-service-type will be > deprecated in favor of the more general oci-service-type, everything is > moved outside of the docker related module. > > * gnu/services/docker.scm: Move everything related to oci-container-configuration > to... > * gnu/services/containers.scm: ...here.scm. This one also. -- Thanks, Maxim
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Thu, 15 May 2025 07:23:01 GMT) Full text and rfc822 format available.Message #191 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: Giacomo Leidi <goodoldpaul <at> autistici.org> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 3/7] tests: oci-container: Set explicit timeouts. Date: Thu, 15 May 2025 16:21:58 +0900
Hi, Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > * gnu/tests/docker.scm: Simplify %test-oci-container test case and add > explicit timeouts to tests outcomes. [...] > (test-assert "docker-guile running" > (marionette-eval > '(begin > (use-modules (gnu services herd)) > - (match (start-service 'docker-guile) > - (#f #f) > - (('service response-parts ...) > - (match (assq-ref response-parts 'running) > - ((pid) pid))))) > + (wait-for-service 'docker-guile #:timeout 120) > + #t) You can drop the trailing '#t'. > marionette)) > > - (test-equal "passing host environment variables and volumes" > - '("value" "hello") > - (marionette-eval > - `(begin > - (use-modules (ice-9 popen) > - (ice-9 rdelim)) > - > - (define slurp > - (lambda args > - (let* ((port (apply open-pipe* OPEN_READ args)) > - (output (let ((line (read-line port))) > - (if (eof-object? line) > - "" > - line))) > - (status (close-pipe port))) > - output))) > - (let* ((response1 (slurp > - ,(string-append #$docker-cli "/bin/docker") > - "exec" "docker-guile" > - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) > - (response2 (slurp > - ,(string-append #$docker-cli "/bin/docker") > - "exec" "docker-guile" > - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) > + (test-assert "passing host environment variables and volumes" > + (begin > + (define (run-test) > + (marionette-eval > + `(begin > + (use-modules (ice-9 popen) > + (ice-9 rdelim)) > + > + (define slurp > + (lambda args > + (let* ((port (apply open-pipe* OPEN_READ args)) > + (output (let ((line (read-line port))) > + (if (eof-object? line) > + "" > + line))) > + (status (close-pipe port))) > + output))) > + (let* ((response1 (slurp > + ,(string-append #$docker-cli "/bin/docker") > + "exec" "docker-guile" > + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) > + (response2 (slurp > + ,(string-append #$docker-cli "/bin/docker") > + "exec" "docker-guile" > + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) Please mind the 80 columns maximum width :-). > (display (call-with-input-file \"/shared.txt\" read-line)))"))) > - (list response1 response2))) > - marionette)) > + (list response1 response2))) > + marionette)) > + ;; Allow services to come up on slower machines Please use complete punctuation for standalone comments. > + (let loop ((attempts 0)) > + (if (= attempts 60) > + (error "Service didn't come up after more than 60 seconds") I'm curious as to why this is necessary, given in the previous test we wait for the docker-guile service to be ready? -- Thanks, Maxim
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Fri, 16 May 2025 01:40:02 GMT) Full text and rfc822 format available.Message #194 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: Giacomo Leidi <goodoldpaul <at> autistici.org> Cc: Ludovic Courtès <ludo <at> gnu.org>, Gabriel Wicki <gabriel <at> erlikon.ch>, 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 4/7] services: Add oci-service-type. Date: Fri, 16 May 2025 10:38:54 +0900
Hi, Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > This patch implements a generalization of the > oci-container-service-type, which consequently is made deprecated. The > oci-service-type, in addition to all the features from the > oci-container-service-type, can now provision OCI networks and volumes. > It only handles OCI objects creation, the user is supposed to handle > state once the objects are provsioned. > > It currently supports two different OCI runtimes: Docker and rootless > Podman. Both runtimes are tested to make sure provisioned containers > can connect to each other through provisioned networks and can > read/write data with provisioned volumes. Sounds useful! > At last the Scheme API is thought to facilitate the implementation of a > Guix Home service in the future. > > * gnu/services/containers.scm (%oci-supported-runtimes): New variable; > (oci-runtime-cli): new variable; > (oci-runtime-name): new variable; > (oci-network-configuration): new variable; > (oci-volume-configuration): new variable; > (oci-configuration): new variable; > (oci-extension): new variable; > (oci-networks-shepherd-name): new variable; > (oci-service-type): new variable; > (oci-state->shepherd-services): new variable. Nitpick: you can combine multiple variables/procedures, like: (oci-runtime-cli, oci-runtime-name) (oci-network-configuration, oci-volume-configuration) [...] (oci-service-type): New variables. The above change log appears incomplete, for example oci-image-reference was changed but isn't listed. [...] > @lisp > -(service oci-container-service-type > - (list > - (oci-container-configuration > - (network "host") > - (image > - (oci-image > - (repository "guile") > - (tag "3") > - (value (specifications->manifest '("guile"))) > - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) > - #:max-layers 2)))) > - (entrypoint "/bin/guile") > - (command > - '("-c" "(display \"hello!\n\")"))) > - (oci-container-configuration > - (image "prom/prometheus") > - (ports > - '(("9000" . "9000") > - ("9090" . "9090")))) > - (oci-container-configuration > - (image "grafana/grafana:10.0.1") > - (network "host") > - (volumes > - '("/var/lib/grafana:/var/lib/grafana"))))) > +(simple-service 'oci-provisioning > + oci-service-type > + (oci-extension > + (networks > + (list > + (oci-network-configuration (name "monitoring")))) > + (containers > + (list > + (oci-container-configuration > + (network "monitoring") > + (image > + (oci-image > + (repository "guile") > + (tag "3") > + (value (specifications->manifest '("guile"))) > + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) > + #:max-layers 2)))) > + (entrypoint "/bin/guile") > + (command > + '("-c" "(display \"hello!\n\")"))) > + (oci-container-configuration > + (image "prom/prometheus") > + (network "host") > + (ports > + '(("9000" . "9000") > + ("9090" . "9090")))) > + (oci-container-configuration > + (image "grafana/grafana:10.0.1") > + (network "host") > + (volumes > + '("/var/lib/grafana:/var/lib/grafana"))))))) I was curious: was is the gain to have by using the extension mechanism instead of directly accepting a list of 'oci-configuration' or similarly named record to the oci-service-type? > @end lisp > > In this example three different Shepherd services are going to be added to the > system. Each @code{oci-container-configuration} record translates to a > -@code{docker run} invocation and its fields directly map to options. You can > -refer to the > -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} > -documentation for the semantics of each value. If the images are not found, > -they will be > -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The > +@command{docker run} or @command{podman run} invocation and its fields directly I know not all of these instances are newly added, but @command should only be used with a single command name. For a command line, use @samp. > +map to options. You can refer to the > +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} > +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} > +upstream documentation for semantics of each value. If the images are not found, > +they will be pulled. You can refer to the > +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} > +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} > +upstream documentation for semantics. The > services with @code{(network "host")} are going to be attached to the > host network and are supposed to behave like native processes with regard to > networking. > > @end defvar > > +@c %start of fragment > + > +@deftp {Data Type} oci-configuration > +Available @code{oci-configuration} fields are: > + > +@table @asis > +@item @code{runtime} (default: @code{'docker}) (type: symbol) > +The OCI runtime to use to run commands. It can be either @code{'docker} or > +@code{'podman}. > + > +@item @code{runtime-cli} (type: maybe-package-or-string) > +The OCI runtime command line to be installed in the system profile and used > +to provision OCI resources, it can be either a package or a string representing > +an absolute path to the runtime binary entrypoint. When unset it will default s/absolute path/absolute file name/ [...] > +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) > +The list of @code{oci-container-configuration} records representing the > +containers to provision. Most users are supposed not to use this field and use > +the @code{oci-extension} record instead. Maybe streamline to 'The use of the @code{oci-extension} record should be preferred for most cases.' As I wondered above, what is the benefit? > + > +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) > +The list of @code{oci-network-configuration} records representing the > +containers to provision. Most users are supposed not to use this field and use > +the @code{oci-extension} record instead. Ditto. > +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) > +The list of @code{oci-volumes-configuration} records representing the > +containers to provision. Most users are supposed not to use this field and use > +the @code{oci-extension} record instead. Ditto. [...] > diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm > index 24f31c756b8..66b46456800 100644 > --- a/gnu/services/containers.scm > +++ b/gnu/services/containers.scm [...] > +(define-configuration/no-serialization oci-volume-configuration > + (name > + (string) > + "The name of the OCI volume to provision.") > + (labels > + (list '()) > + "The list of labels that will be used to tag the current volume." > + (sanitizer oci-sanitize-labels)) > + (extra-arguments > + (list '()) > + "A list of strings, gexps or file-like objects that will be directly passed > +to the @command{docker volume create} or @command{podman volume create} > +invocation." > + (sanitizer oci-sanitize-extra-arguments))) > + > +(define (list-of-oci-volumes? value) > + (list-of-oci-records? "volumes" oci-volume-configuration? value)) > + > +(define-configuration/no-serialization oci-network-configuration > + (name > + (string) > + "The name of the OCI network to provision.") > + (driver > + (maybe-string) > + "The driver to manage the network.") > + (gateway > + (maybe-string) > + "IPv4 or IPv6 gateway for the subnet.") > + (internal? > + (boolean #f) > + "Restrict external access to the network") > + (ip-range > + (maybe-string) > + "Allocate container ip from a sub-range in CIDR format.") > + (ipam-driver > + (maybe-string) > + "IP Address Management Driver.") > + (ipv6? > + (boolean #f) > + "Enable IPv6 networking.") > + (subnet > + (maybe-string) > + "Subnet in CIDR format that represents a network segment.") > + (labels > + (list '()) > + "The list of labels that will be used to tag the current volume." > + (sanitizer oci-sanitize-labels)) > + (extra-arguments > + (list '()) > + "A list of strings, gexps or file-like objects that will be directly passed > +to the @command{docker network create} or @command{podman network create} > +invocation." > + (sanitizer oci-sanitize-extra-arguments))) > + > +(define (list-of-oci-networks? value) > + (list-of-oci-records? "networks" oci-network-configuration? value)) > + > +(define (package-or-string? value) > + (or (package? value) (string? value))) > + > +(define-maybe/no-serialization package-or-string) > + > +(define-configuration/no-serialization oci-configuration > + (runtime > + (symbol 'docker) > + "The OCI runtime to use to run commands. It can be either @code{'docker} or > +@code{'podman}." > + (sanitizer oci-sanitize-runtime)) Instead of using explicit sanitizers, it'd be better to define a oci-runtime? predicate and use it as a type in the configuration. This way errors are reported with source location information. [...] > + > +(define (oci-runtime-system-environment runtime user) > + (if (eq? runtime 'podman) > + (list > + #~(string-append > + "HOME=" (passwd:dir (getpwnam #$user)))) > + #~())) > + > +(define (oci-runtime-cli runtime runtime-cli path) > + "Return a gexp that, when lowered, evaluates to the file system path of the OCI > +runtime command requested by the user." > + (if (string? runtime-cli) > + ;; It is a user defined absolute path > + runtime-cli > + #~(string-append > + #$(if (not (maybe-value-set? runtime-cli)) > + path > + runtime-cli) > + #$(if (eq? 'podman runtime) > + "/bin/podman" > + "/bin/docker")))) > + > +(define* (oci-runtime-system-cli config #:key (path "/run/current-system/profile")) > + (let ((runtime-cli > + (oci-configuration-runtime-cli config)) > + (runtime > + (oci-configuration-runtime config))) > + (oci-runtime-cli runtime runtime-cli path))) > + > +(define (oci-runtime-home-cli config) > + (let ((runtime-cli > + (oci-configuration-runtime-cli config)) > + (runtime > + (oci-configuration-runtime config))) > + (oci-runtime-cli runtime runtime-cli > + (string-append (getenv "HOME") > + "/.guix-home/profile")))) I'd replace the 'path' instances in the source with 'file' or 'file-name', to better match our GNU conventions. [...] > +(define (oci-object-exists? runtime runtime-cli object verbose?) > + #~(lambda* (name #:key (format-string "{{.Name}}")) > + (use-modules (ice-9 format) > + (ice-9 match) > + (ice-9 popen) > + (ice-9 rdelim) > + (srfi srfi-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))) > + > + #$(if (eq? runtime 'podman) > + #~(let ((command > + (list #$runtime-cli > + #$object "exists" name))) > + (when #$verbose? > + (format #t "Running~{ ~a~}~%" command)) > + (define exit-code (status:exit-val (apply system* command))) > + (when #$verbose? > + (format #t "Exit code: ~a~%" exit-code)) > + (equal? EXIT_SUCCESS exit-code)) > + #~(let ((command > + (string-append #$runtime-cli > + " " #$object " ls --format " > + "\"" format-string "\""))) > + (when #$verbose? > + (format #t "Running ~a~%" command)) > + (member name (read-lines (open-input-pipe command))))))) That's a complex piece of staged code; have you considered placing it in a supporting build-side module, e.g. gnu/build/oci-containers.scm ? Then it could be more simply imported in the gexp environment and used. See the gnu/build/jami-service.scm for an example. > +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) > + "Return a file-like object that, once lowered, will evaluate to a program able > +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." > + (let ((tarball (lower-oci-image name image))) > (with-imported-modules '((guix build utils)) > - (program-file (format #f "~a-image-loader" name) > + (program-file > + (format #f "~a-image-loader" name) > #~(begin > (use-modules (guix build utils) > + (ice-9 match) > (ice-9 popen) > - (ice-9 rdelim)) > - > - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) > - (define line > - (read-line > - (open-input-pipe > - (string-append #$docker " load -i " #$tarball)))) > - > - (unless (or (eof-object? line) > - (string-null? line)) > - (format #t "~a~%" line) > - (let ((repository&tag > - (string-drop line > - (string-length > - "Loaded image: ")))) > - > - (invoke #$docker "tag" repository&tag #$tag) > - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) > - > -(define (oci-container-shepherd-service config) > - (define (guess-name name image) > - (if (maybe-value-set? name) > - name > - (string-append "docker-" > - (basename > - (if (string? image) > - (first (string-split image #\:)) > - (oci-image-repository image)))))) > - > - (let* ((docker (file-append docker-cli "/bin/docker")) > - (actions (oci-container-configuration-shepherd-actions config)) > + (ice-9 rdelim) > + (srfi srfi-1)) > + (define object-exists? > + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) > + (define load-command > + (string-append #$runtime-cli > + " load -i " #$tarball)) > + > + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") > + (format #t "~a image already exists, skipping.~%" #$tag) > + (begin > + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) > + (when #$verbose? > + (format #t "Running ~a~%" load-command)) > + (let ((line (read-line > + (open-input-pipe load-command)))) > + (unless (or (eof-object? line) > + (string-null? line)) > + (format #t "~a~%" line) > + (let* ((repository&tag > + (string-drop line > + (string-length > + "Loaded image: "))) > + (tag-command > + (list #$runtime-cli "tag" repository&tag #$tag)) > + (drop-old-tag-command > + (list #$runtime-cli "image" "rm" "-f" repository&tag))) > + > + (unless (string=? repository&tag #$tag) > + (when #$verbose? > + (format #t "Running~{ ~a~}~%" tag-command)) > + > + (let ((exit-code > + (status:exit-val (apply system* tag-command)))) > + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) > + > + (when #$verbose? > + (format #t "Exit code: ~a~%" exit-code)) > + > + (when (equal? EXIT_SUCCESS exit-code) > + (when #$verbose? > + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) > + (let ((drop-exit-code > + (status:exit-val (apply system* drop-old-tag-command)))) > + (when #$verbose? > + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) > + > +(define (oci-container-run-invocation runtime runtime-cli name command image-reference > + options runtime-extra-arguments run-extra-arguments) That's a lot of arguments! Perhaps it could accept a record instead? Please also mind the 80 chars max width. > + "Return a list representing the OCI runtime > +invocation for running containers." > + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] > + `(,runtime-cli ,@runtime-extra-arguments "run" "--rm" > + ,@(if (eq? runtime 'podman) > + ;; This is because podman takes some time to > + ;; release container names. --replace seems > + ;; to be required to be able to restart services. > + '("--replace") > + '()) > + "--name" ,name > + ,@options ,@run-extra-arguments > + ,image-reference ,@command)) > + > +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference > + invocation #:key (verbose? #f) (pre-script #~())) The default value for a keyword argument is #f, so I'd omit the explicit default for verbose?. > + "Return a file-like object that, once lowered, will evaluate to the entrypoint > +for the Shepherd service that will run IMAGE through RUNTIME-CLI." > + (program-file > + (string-append "oci-entrypoint-" name) > + #~(begin > + (use-modules (ice-9 format) > + (srfi srfi-1)) > + (when #$verbose? > + (format #t "Running in verbose mode...~%") > + (format #t "Current user: ~a ~a~%" > + (getuid) (passwd:name (getpwuid (getuid)))) > + (format #t "Current group: ~a ~a~%" > + (getgid) (group:name (getgrgid (getgid)))) > + (format #t "Current directory ~a~%" (getcwd))) I'd use a single format call; you can format the string naturally like (format #t "\ Running in verbose mode... Current user: ~a ~a ... " arg1 arg2, ...) > + (define invocation (list #$@invocation)) > + #$@pre-script > + (when #$verbose? > + (format #t "Running~{ ~a~}~%" invocation)) > + (apply execlp `(,(first invocation) ,@invocation))))) You can more simply: (apply execlp (first invocation) invocation) > + > +(define* (oci-container-shepherd-service runtime runtime-cli config > + #:key > + (runtime-environment #~()) > + (runtime-extra-arguments '()) > + (oci-requirement '()) > + (user #f) > + (group #f) > + (verbose? #f)) > + "Return a Shepherd service object that will run the OCI container represented > +by CONFIG through RUNTIME-CLI." > + (let* ((actions (oci-container-configuration-shepherd-actions config)) > (auto-start? > (oci-container-configuration-auto-start? config)) > - (user (oci-container-configuration-user config)) > - (group (oci-container-configuration-group config)) > + (maybe-user (oci-container-configuration-user config)) > + (maybe-group (oci-container-configuration-group config)) > + (user (if (maybe-value-set? maybe-user) > + maybe-user > + user)) > + (group (if (maybe-value-set? maybe-group) > + maybe-group > + group)) > (host-environment > (oci-container-configuration-host-environment config)) > (command (oci-container-configuration-command config)) > (log-file (oci-container-configuration-log-file config)) > - (provision (oci-container-configuration-provision config)) > (requirement (oci-container-configuration-requirement config)) > (respawn? > (oci-container-configuration-respawn? config)) > (image (oci-container-configuration-image config)) > (image-reference (oci-image-reference image)) > (options (oci-container-configuration->options config)) > - (name (guess-name provision image)) > + (name > + (oci-container-shepherd-name runtime config)) > (extra-arguments > - (oci-container-configuration-extra-arguments config))) > + (oci-container-configuration-extra-arguments config)) > + (invocation > + (oci-container-run-invocation > + runtime runtime-cli name command image-reference > + options runtime-extra-arguments extra-arguments)) > + (container-action > + (lambda* (command #:key (environment-variables #f)) > + #~(lambda _ > + (fork+exec-command > + (list #$@command) > + #$@(if user (list #:user user) '()) > + #$@(if group (list #:group group) '()) > + #$@(if (maybe-value-set? log-file) > + (list #:log-file log-file) > + '()) > + #$@(if (and user (eq? runtime 'podman)) > + (list #:directory > + #~(passwd:dir (getpwnam #$user))) > + '()) > + #$@(if environment-variables > + (list #:environment-variables > + environment-variables) > + '())))))) That's a very long let. Since many values appear to simply be accessed from theh oci-container-configuration, match-record could be used to great effect. For readability, I'd also define the action procedures using nested 'define' inside the let/match-record. [...] [...] > + (begin > + (when #$verbose? > + (format #t "Running~{ ~a~}~%" invocation)) > + (let ((exit-code (status:exit-val (apply system* invocation)))) > + (when #$verbose? > + (format #t "Exit code: ~a~%" exit-code)))))) > + (list #$@invocations))))) > + > +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invocations Max width :-). > + #:key > + (runtime-environment #~()) > + (user #f) > + (group #f) > + (verbose? #f)) I'd drop the default #f values, here and elsewhere. [...] > +(define (oci-service-subids config) > + "Return a subids-extension record representing subuids and subgids required by > +the rootless Podman backend." > + (define (delete-duplicate-ranges ranges) > + (delete-duplicates ranges > + (lambda args > + (apply string=? (map subid-range-name ranges))))) I don't understand how the above 'delete-duplicate-ranges' procedure works; the 2nd argument of delete-duplicates is normally a predicate that accepts two values, but here it's a lambda that disregards the arguments: it seems it'd be wasteful to compute the same result over and over? > + (define runtime > + (oci-configuration-runtime config)) > + (define user > + (oci-configuration-user config)) > + (define subgids (oci-configuration-subgids-range config)) > + (define subuids (oci-configuration-subuids-range config)) > + (define container-users > + (filter (lambda (range) > + (and (maybe-value-set? > + (subid-range-name range)) > + (not (string=? (subid-range-name range) user)))) > + (map (lambda (container) > + (subid-range > + (name > + (oci-container-configuration-user container)))) > + (oci-configuration-containers config)))) > + (define subgid-ranges > + (delete-duplicate-ranges > + (cons > + (if (not (maybe-value-set? subgids)) > + (subid-range (name user)) > + subgids) > + container-users))) > + (define subuid-ranges > + (delete-duplicate-ranges > + (cons > + (if (not (maybe-value-set? subuids)) Here and above, I'd reverse your if branches, so you do not need to use 'not'. > + (subid-range (name user)) > + subuids) > + container-users))) > + > + (if (eq? 'podman runtime) > + (subids-extension > + (subgids > + subgid-ranges) > + (subuids > + subuid-ranges)) > + (subids-extension))) > + > +(define (oci-objects-merge-lst a b object get-name) > + (define (contains? value lst) > + (member value (map get-name lst))) > + (let loop ((merged '()) > + (lst (append a b))) > + (if (null? lst) > + merged > + (loop > + (let ((element (car lst))) > + (when (contains? element merged) > + (raise > + (formatted-message > + (G_ "Duplicated ~a: ~a. ~as names should be unique, please s/~as names/names of ~a/, perhaps? > +remove the duplicate.") object (get-name element) object))) > + (cons element merged)) > + (cdr lst))))) > + > +(define (oci-extension-merge a b) > + (oci-extension > + (containers (oci-objects-merge-lst > + (oci-extension-containers a) > + (oci-extension-containers b) > + "container" > + (lambda (config) > + (define maybe-name (oci-container-configuration-provision config)) Max width. [...] > +(define oci-service-type > + (service-type (name 'oci) > + (extensions > + (list > + (service-extension profile-service-type > + (lambda (config) > + (let ((runtime-cli > + (oci-configuration-runtime-cli config)) > + (runtime > + (oci-configuration-runtime config))) > + (oci-service-profile runtime runtime-cli)))) You can nest the fields under (service-type to meet the max columns width. > + (service-extension subids-service-type > + oci-service-subids) > + (service-extension account-service-type > + oci-service-accounts) > + (service-extension shepherd-root-service-type > + oci-configuration->shepherd-services))) > + ;; Concatenate OCI object lists. > + (compose (lambda (args) > + (fold oci-extension-merge > + (oci-extension) > + args))) > + (extend oci-configuration-extend) > + (default-value (oci-configuration)) > + (description > + "This service implements the provisioning of OCI object such s/OCI object/OCI objects/ > +as containers, networks and volumes."))) > diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm > index 828ceea313a..125e748bb0e 100644 > --- a/gnu/services/docker.scm > +++ b/gnu/services/docker.scm > @@ -31,7 +31,10 @@ (define-module (gnu services docker) > #:use-module (gnu system shadow) > #:use-module (gnu packages docker) > #:use-module (gnu packages linux) ;singularity > + #:use-module (guix deprecation) > + #:use-module (guix diagnostics) > #:use-module (guix gexp) > + #:use-module (guix i18n) > #:use-module (guix records) > #:use-module (srfi srfi-1) > #:use-module (ice-9 format) > @@ -67,16 +70,18 @@ (define-module (gnu services docker) > oci-container-configuration-volumes > oci-container-configuration-container-user > oci-container-configuration-workdir > - oci-container-configuration-extra-arguments > - oci-container-shepherd-service > - %oci-container-accounts) > + oci-container-configuration-extra-arguments) > > #:export (containerd-configuration > containerd-service-type > docker-configuration > docker-service-type > singularity-service-type > - oci-container-service-type)) > + ;; for backwards compatibility, until the > + ;; oci-container-service-type is fully deprecated Please punctuate fully standalone comments (here, capitalizing the first letter and adding a trailing period). > + oci-container-shepherd-service > + oci-container-service-type > + %oci-container-accounts)) > > (define-maybe file-like) > > @@ -297,17 +302,25 @@ (define singularity-service-type > ;;; OCI container. > ;;; > > -(define (configs->shepherd-services configs) > - (map oci-container-shepherd-service configs)) > +;; for backwards compatibility, until the > +;; oci-container-service-type is fully deprecated Ditto. > +(define-deprecated (oci-container-shepherd-service config) > + oci-service-type > + ((@ (gnu services containers) oci-container-shepherd-service) > + 'docker config)) > +(define %oci-container-accounts > + (filter user-account? (oci-service-accounts (oci-configuration)))) > > (define oci-container-service-type > (service-type (name 'oci-container) > - (extensions (list (service-extension profile-service-type > - (lambda _ (list docker-cli))) > - (service-extension account-service-type > - (const %oci-container-accounts)) > - (service-extension shepherd-root-service-type > - configs->shepherd-services))) > + (extensions > + (list (service-extension oci-service-type > + (lambda (containers) > + (warning > + (G_ > + "'oci-container-service-type' is deprecated, use 'oci-service-type' instead~%")) Max width; you can break long strings with \ (backslash) escape. > + (oci-extension > + (containers containers)))))) > (default-value '()) > (extend append) > (compose concatenate) > diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm > index 0ecc8ddb126..5e6f39387e7 100644 > --- a/gnu/tests/containers.scm > +++ b/gnu/tests/containers.scm [...] > +(define (run-rootless-podman-oci-service-test) > + (define os > + (marionette-operating-system > + (operating-system-with-gc-roots > + %oci-rootless-podman-os > + (list)) > + #: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)) > + #~(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-oci-service") > + > + (marionette-eval > + '(begin > + (use-modules (gnu services herd)) > + (wait-for-service 'user-processes)) > + marionette) > + > + (test-assert "rootless-podman services started successfully" > + (begin > + (define (run-test) > + (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")) Unquote-splice is not necessary here, you can more directly #$(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")))) Is there perhaps an easier way to check that the podman services were started successfully? Perhaps using the podman command directly to query the state of the running containers? 'ls' is usually frowned for scripting; it's meant for interactive use. Perhaps you could use the shell wildcard feature or the find command instead? I comment explaining what the above does/check exactly would be useful; it's not obvious. > + (list (string-join response1 "\n") (string-join response2 "\n")))) > + marionette)) > + ;; Allow services to come up on slower machines ^ missing period > + (let loop ((attempts 0)) > + (if (= attempts 60) > + (error "Services didn't come up after more than 60 seconds") > + (if (equal? '("cgroup" "cgroup") > + (run-test)) > + #t > + (begin > + (sleep 1) > + (format #t "Services didn't come up yet, retrying with attempt ~a~%" > + (+ 1 attempts)) > + (loop (+ 1 attempts)))))))) Note that we have a 'with-retries' definition in (gnu build jami-service) which could be used here; perhaps it should be relocated elsewhere. > + > + (test-assert "podman-volumes running" > + (begin > + (define (run-test) > + (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 6)) > + (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 "oci-container"))) > + (setuid (passwd:uid (getpw "oci-container"))) > + > + (let ((response (slurp > + "/run/current-system/profile/bin/podman" > + "volume" "ls" "-n" "--format" "\"{{.Name}}\"" > + "|" "tr" "' '" "'\n'"))) > + > + (call-with-output-file (string-append ,out-dir "/response") I'd remove 'out-dir' and hard-code "/tmp/response" directly. > + (lambda (port) > + (display (string-join response "\n") port))))) > + (lambda () > + (primitive-exit 127)))) The fork is used to be able to run the podman command with the right passwd and uid, right? Maybe you could use the shepherd's provided 'command' or 'fork+exec-command', which have a nice API to do this. Also, is the dynamic-wind unnecessary? I would have thought that if a forked children aborts, its exit status should be reflected accordingly? > + (pid > + (cdr (waitpid pid)))) > + > + (wait-for-file (string-append ,out-dir "/response")) > + > + (stable-sort > + (slurp "cat" (string-append ,out-dir "/response")) > + string<=?)) > + marionette)) > + ;; Allow services to come up on slower machines missing trailing '.'. > + (let loop ((attempts 0)) > + (if (= attempts 80) > + (error "Service didn't come up after more than 80 seconds") nitpick: error messages do not need to be complete sentences/be punctuated, as they are intended for debugging. > + (if (equal? '("my-volume") > + (run-test)) > + #t > + (begin > + (sleep 1) > + (loop (+ 1 attempts)))))))) > + > + (test-assert "podman-networks running" > + (begin > + (define (run-test) > + (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 6)) > + (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)) The definitions boilerplate could be defined as data above the tests and spliced in for reuse. > + (match (primitive-fork) > + (0 > + (dynamic-wind > + (const #f) > + (lambda () > + (setgid (passwd:gid (getpwnam "oci-container"))) > + (setuid (passwd:uid (getpw "oci-container"))) > + > + (let ((response (slurp > + "/run/current-system/profile/bin/podman" > + "network" "ls" "-n" "--format" "\"{{.Name}}\"" > + "|" "tr" "' '" "'\n'"))) Max width :-). > + (call-with-output-file (string-append ,out-dir "/response") > + (lambda (port) > + (display (string-join response "\n") port))))) > + (lambda () > + (primitive-exit 127)))) > + (pid > + (cdr (waitpid pid)))) > + > + (wait-for-file (string-append ,out-dir "/response")) > + > + (stable-sort > + (slurp "cat" (string-append ,out-dir "/response")) > + string<=?)) I don't think using the stable variant here is necessary/useful. > + marionette)) > + ;; Allow services to come up on slower machines > + (let loop ((attempts 0)) > + (if (= attempts 80) > + (error "Service didn't come up after more than 80 seconds") > + (if (equal? '("my-network" "podman") > + (run-test)) > + #t > + (begin > + (sleep 1) > + (loop (+ 1 attempts)))))))) > + > + (test-assert "first container running" > + (marionette-eval > + '(begin > + (use-modules (gnu services herd)) > + (wait-for-service 'first #:timeout 120) > + #t) No need for #t. > + marionette)) > + > + (test-assert "second container running" > + (marionette-eval > + '(begin > + (use-modules (gnu services herd)) > + (wait-for-service 'second #:timeout 120) > + #t) Ditto. > + marionette)) > + > + (test-assert "passing host environment variables" > + (begin > + (define (run-test) > + (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 "oci-container"))) > + (setuid (passwd:uid (getpw "oci-container"))) > + > + (let ((response (slurp > + "/run/current-system/profile/bin/podman" > + "exec" "first" > + "/bin/guile" "-c" "'(display (getenv \"VARIABLE\"))'"))) > + (call-with-output-file (string-append ,out-dir "/response") > + (lambda (port) > + (display (string-join response "\n") port))))) > + (lambda () > + (primitive-exit 127)))) > + (pid > + (cdr (waitpid pid)))) > + > + (wait-for-file (string-append ,out-dir "/response")) > + (slurp "cat" (string-append ,out-dir "/response"))) > + marionette)) > + ;; Allow image to be loaded on slower machines > + (let loop ((attempts 0)) > + (if (= attempts 180) > + (error "Service didn't come up after more than 180 seconds") > + (if (equal? (list "value") > + (run-test)) > + #t > + (begin > + (sleep 1) > + (loop (+ 1 attempts)))))))) I'm curious, did you test this on a slow machine to see how much time it needed there? > + (test-equal "mounting host files" [...] > + (match (primitive-fork) > + (0 > + (dynamic-wind > + (const #f) > + (lambda () > + (setgid (passwd:gid (getpwnam "oci-container"))) > + (setuid (passwd:uid (getpw "oci-container"))) > + > + (let ((response (slurp > + "/run/current-system/profile/bin/podman" > + "exec" "second" > + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) > +(display (call-with-input-file \"/shared.txt\" read-line)))'"))) > + (call-with-output-file (string-append ,out-dir "/response") > + (lambda (port) > + (display (string-join response " ") port))))) > + (lambda () > + (primitive-exit 127)))) > + (pid > + (cdr (waitpid pid)))) > + > + (wait-for-file (string-append ,out-dir "/response")) > + (slurp "cat" (string-append ,out-dir "/response"))) > + marionette)) > + > + (test-equal "write to volumes" > + '("world") > + (marionette-eval [...] > + (match (primitive-fork) > + (0 > + (dynamic-wind > + (const #f) > + (lambda () > + (setgid (passwd:gid (getpwnam "oci-container"))) > + (setuid (passwd:uid (getpw "oci-container"))) > + > + (slurp > + "/run/current-system/profile/bin/podman" > + "exec" "first" > + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) > +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))'") > + > + (let ((response (slurp > + "/run/current-system/profile/bin/podman" > + "exec" "second" > + "/bin/guile" "-c" "'(begin (use-modules (ice-9 popen) (ice-9 rdelim)) > +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))'"))) > + (call-with-output-file (string-append ,out-dir "/response") > + (lambda (port) > + (display (string-join response " ") port))))) > + (lambda () > + (primitive-exit 127)))) > + (pid > + (cdr (waitpid pid)))) The idiom of forking ot invoke podman is used so commonly it should be extracted to a procedure or syntax and reused. > + > + (wait-for-file (string-append ,out-dir "/response")) > + (slurp "cat" (string-append ,out-dir "/response"))) > + marionette)) > + > + (test-equal "can read ports over network" > + '("out of office") > + (marionette-eval > + `(begin [...] > + (match (primitive-fork) > + (0 > + (dynamic-wind > + (const #f) > + (lambda () > + (setgid (passwd:gid (getpwnam "oci-container"))) > + (setuid (passwd:uid (getpw "oci-container"))) > + > + (let ((response (slurp > + "/run/current-system/profile/bin/podman" > + "exec" "second" > + "/bin/guile" "-c" "'(begin (use-modules (web client)) > +(define-values (response out) > + (http-get \"http://first:8080\")) > +(display out))'"))) > + (call-with-output-file (string-append ,out-dir "/response") > + (lambda (port) > + (display (string-join response " ") port))))) > + (lambda () > + (primitive-exit 127)))) > + (pid > + (cdr (waitpid pid)))) > + > + (wait-for-file (string-append ,out-dir "/response")) > + (slurp "cat" (string-append ,out-dir "/response"))) > + marionette)) > + > + (test-end)))) > + > + (gexp->derivation "rootless-podman-oci-service-test" test)) > + > +(define %test-oci-service-rootless-podman > + (system-test > + (name "oci-service-rootless-podman") > + (description "Test Rootless-Podman backed OCI provisioning service.") > + (value (run-rootless-podman-oci-service-test)))) > + > +(define %oci-docker-os > + (simple-operating-system > + (service dhcp-client-service-type) > + (service dbus-root-service-type) > + (service polkit-service-type) > + (service elogind-service-type) > + (service containerd-service-type) > + (service docker-service-type) > + (extra-special-file "/shared.txt" > + (plain-file "shared.txt" "hello")) > + (service oci-service-type > + (oci-configuration > + (verbose? #t))) > + (simple-service 'oci-provisioning > + oci-service-type > + (oci-extension > + (networks > + (list (oci-network-configuration (name "my-network")))) > + (volumes > + (list (oci-volume-configuration (name "my-volume")))) > + (containers > + (list > + (oci-container-configuration > + (provision "first") > + (image > + (oci-image > + (repository "guile") > + (value > + (specifications->manifest '("guile"))) > + (pack-options > + '(#:symlinks (("/bin" -> "bin")))))) > + (entrypoint "/bin/guile") > + (network "my-network") > + (command > + '("-c" "(use-modules (web server)) > +(define (handler request request-body) > + (values '((content-type . (text/plain))) \"out of office\")) > +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) > + (host-environment > + '(("VARIABLE" . "value"))) > + (volumes > + '(("my-volume" . "/my-volume"))) > + (extra-arguments > + '("--env" "VARIABLE"))) > + (oci-container-configuration > + (provision "second") > + (image > + (oci-image > + (repository "guile") > + (value > + (specifications->manifest '("guile"))) > + (pack-options > + '(#:symlinks (("/bin" -> "bin")))))) > + (entrypoint "/bin/guile") > + (network "my-network") > + (command > + '("-c" "(let l ((c 300))(display c)(sleep 1)(when(positive? c)(l (- c 1))))")) You can indent the snippet normally on multiple lines, for readability, or extract it as some expression to splice in. > + (volumes > + '(("my-volume" . "/my-volume") > + ("/shared.txt" . "/shared.txt:ro")))))))))) > + > +(define (run-docker-oci-service-test) > + (define os > + (marionette-operating-system > + (operating-system-with-gc-roots > + %oci-docker-os > + (list)) > + #: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)) > + #~(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)) > + > + (test-runner-current (system-test-runner #$output)) > + (test-begin "docker-oci-service") > + > + (marionette-eval > + '(begin > + (use-modules (gnu services herd)) > + (wait-for-service 'dockerd)) > + marionette) > + > + (test-assert "docker-volumes running" > + (begin > + (define (run-test) > + (marionette-eval > + `(begin > + (use-modules (ice-9 popen) > + (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 > + (list "sh" "-l" "-c" > + (string-join > + args > + " ")))) > + (output (read-lines port)) > + (status (close-pipe port))) > + output))) > + > + (stable-sort > + (slurp > + "/run/current-system/profile/bin/docker" > + "volume" "ls" "--format" "\"{{.Name}}\"") > + string<=?)) > + > + marionette)) > + ;; Allow services to come up on slower machines > + (let loop ((attempts 0)) > + (if (= attempts 80) > + (error "Service didn't come up after more than 80 seconds") > + (if (equal? '("my-volume") > + (run-test)) > + #t > + (begin > + (sleep 1) > + (loop (+ 1 attempts)))))))) > + > + (test-assert "docker-networks running" > + (begin > + (define (run-test) > + (marionette-eval > + `(begin > + (use-modules (ice-9 popen) > + (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 > + (list "sh" "-l" "-c" > + (string-join > + args > + " ")))) > + (output (read-lines port)) > + (status (close-pipe port))) > + output))) > + > + (stable-sort > + (slurp > + "/run/current-system/profile/bin/docker" > + "network" "ls" "--format" "\"{{.Name}}\"") > + string<=?)) > + > + marionette)) > + ;; Allow services to come up on slower machines > + (let loop ((attempts 0)) > + (if (= attempts 80) > + (error "Service didn't come up after more than 80 seconds") > + (if (equal? '("bridge" "host" "my-network" "none") > + (run-test)) > + #t > + (begin > + (sleep 1) > + (loop (+ 1 attempts)))))))) > + > + (test-assert "first container running" > + (marionette-eval > + '(begin > + (use-modules (gnu services herd)) > + (wait-for-service 'first #:timeout 120) > + #t) > + marionette)) > + > + (test-assert "second container running" > + (marionette-eval > + '(begin > + (use-modules (gnu services herd)) > + (wait-for-service 'second #:timeout 120) > + #t) > + marionette)) > + > + (test-assert "passing host environment variables" > + (begin > + (define (run-test) > + (marionette-eval > + `(begin > + (use-modules (ice-9 popen) > + (ice-9 rdelim)) > + > + (define slurp > + (lambda args > + (let* ((port (apply open-pipe* OPEN_READ args)) > + (output (let ((line (read-line port))) > + (if (eof-object? line) > + "" > + line))) > + (status (close-pipe port))) > + output))) > + > + (slurp > + "/run/current-system/profile/bin/docker" > + "exec" "first" > + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) > + marionette)) > + ;; Allow image to be loaded on slower machines > + (let loop ((attempts 0)) > + (if (= attempts 180) > + (error "Service didn't come up after more than 180 seconds") > + (if (equal? "value" > + (run-test)) > + #t > + (begin > + (sleep 1) > + (loop (+ 1 attempts)))))))) > + > + (test-equal "mounting host files" > + "hello" > + (marionette-eval > + `(begin > + (use-modules (ice-9 popen) > + (ice-9 rdelim)) > + > + (define slurp > + (lambda args > + (let* ((port (apply open-pipe* OPEN_READ args)) > + (output (let ((line (read-line port))) > + (if (eof-object? line) > + "" > + line))) > + (status (close-pipe port))) > + output))) > + > + (slurp > + "/run/current-system/profile/bin/docker" > + "exec" "second" > + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) > +(display (call-with-input-file \"/shared.txt\" read-line)))")) > + marionette)) > + > + (test-equal "write to volumes" > + "world" > + (marionette-eval > + `(begin > + (use-modules (ice-9 popen) > + (ice-9 rdelim)) > + > + (define slurp > + (lambda args > + (let* ((port (apply open-pipe* OPEN_READ args)) > + (output (let ((line (read-line port))) > + (if (eof-object? line) > + "" > + line))) > + (status (close-pipe port))) > + output))) > + > + (slurp > + "/run/current-system/profile/bin/docker" > + "exec" "first" > + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) > +(call-with-output-file \"/my-volume/out.txt\" (lambda (p) (display \"world\" p))))") > + (slurp > + "/run/current-system/profile/bin/docker" > + "exec" "second" > + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) > +(display (call-with-input-file \"/my-volume/out.txt\" read-line)))")) > + marionette)) > + > + (test-equal "can read ports over network" > + "out of office" > + (marionette-eval > + `(begin > + (use-modules (ice-9 popen) > + (ice-9 rdelim)) > + > + (define slurp > + (lambda args > + (let* ((port (apply open-pipe* OPEN_READ args)) > + (output (let ((line (read-line port))) > + (if (eof-object? line) > + "" > + line))) > + (status (close-pipe port))) > + output))) > + > + (slurp > + "/run/current-system/profile/bin/docker" > + "exec" "second" > + "/bin/guile" "-c" "(begin (use-modules (web client)) > +(define-values (response out) > + (http-get \"http://first:8080\")) > +(display out))")) > + marionette)) > + > + (test-end)))) > + > + (gexp->derivation "docker-oci-service-test" test)) > + > +(define %test-oci-service-docker > + (system-test > + (name "oci-service-docker") > + (description "Test Docker backed OCI provisioning service.") > + (value (run-docker-oci-service-test)))) Make sure to mirror my earlier comments to the docker test as well. Phew, that was a large chunk! I'll continue my review with the remaining commits, but when I'm done, could you please submit a v11, taking into account my comments? -- Thanks, Maxim
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Fri, 16 May 2025 01:46:02 GMT) Full text and rfc822 format available.Message #197 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: Giacomo Leidi <goodoldpaul <at> autistici.org> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 5/7] tests: Use lower-oci-image-state in container tests. Date: Fri, 16 May 2025 10:45:32 +0900
Hi, Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > This patch replaces boilerplate in container related tests with > oci-image plumbing from (gnu services containers). > > * gnu/services/containers.scm: Export lower-oci-image-state. I don't think that's a nice API to export; you can instead use the (define lower-oci-image-state (@@ (gnu services containers) lower-oci-image-state) trick to expose internals (white box testing). > * gnu/tests/containers.scm (%oci-tarball): New variable; > (run-rootless-podman-test): use %oci-tarball; > (build-tarball&run-rootless-podman-test): drop procedure. > * gnu/tests/docker.scm (%docker-tarball): New variable; > (build-tarball&run-docker-test): use %docker-tarball; > (%docker-system-tarball): New variable; > (build-tarball&run-docker-system-test): new procedure. > > Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a > --- > gnu/services/containers.scm | 2 + > gnu/tests/containers.scm | 80 ++++++++++++++--------------- > gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- > 3 files changed, 95 insertions(+), 87 deletions(-) The diffstats suggest the result is not that impactful in terms of reducing boilerplate, ah! > diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm > index 66b46456800..a8d10d842da 100644 > --- a/gnu/services/containers.scm > +++ b/gnu/services/containers.scm > @@ -74,6 +74,8 @@ (define-module (gnu services containers) > oci-image-system > oci-image-grafts? > > + lower-oci-image-state > + > oci-container-configuration > oci-container-configuration? > oci-container-configuration-fields > diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm > index 5e6f39387e7..8cdd86e7ae3 100644 > --- a/gnu/tests/containers.scm > +++ b/gnu/tests/containers.scm > @@ -69,13 +69,47 @@ (define %rootless-podman-os > (supplementary-groups '("wheel" "netdev" "cgroup" > "audio" "video"))))))) > > -(define (run-rootless-podman-test oci-tarball) > +(define %oci-tarball > + (lower-oci-image-state > + "guile-guest" > + (packages->manifest > + (list > + guile-3.0 guile-json-3 > + (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"))) I'd use a gexp for the builder expression here, with gexp variables, e.g. #$output. > + (mkdir out) > + (call-with-output-file (string-append out "/a.scm") > + (lambda (port) > + (display "(display \"hello world\n\")" port))) > + #t))) Trailing #t are no longer part of phases/builders in Guix :-). > + (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)))) > + '(#:entry-point "bin/guile" > + #:localstatedir? #t > + #:extra-options (#:image-tag "guile-guest") > + #:symlinks (("/bin/Guile" -> "bin/guile") > + ("aa.scm" -> "a.scm"))) > + "guile-guest" > + (%current-target-system) > + (%current-system) > + #f)) > + > +(define (run-rootless-podman-test) > > (define os > (marionette-operating-system > (operating-system-with-gc-roots > %rootless-podman-os > - (list oci-tarball)) > + (list %oci-tarball)) > #:imported-modules '((gnu services herd) > (guix combinators)))) > > @@ -254,7 +288,7 @@ (define (run-rootless-podman-test oci-tarball) > (let* ((loaded (slurp ,(string-append #$podman > "/bin/podman") > "load" "-i" > - ,#$oci-tarball)) > + ,#$%oci-tarball)) > (repository&tag "localhost/guile-guest:latest") > (response1 (slurp > ,(string-append #$podman "/bin/podman") > @@ -307,49 +341,11 @@ (define (run-rootless-podman-test oci-tarball) > > (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)))) > + (value (run-rootless-podman-test)))) > > > (define %oci-rootless-podman-os > diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm > index 5dcf05a17e3..07edd9d5341 100644 > --- a/gnu/tests/docker.scm > +++ b/gnu/tests/docker.scm > @@ -26,6 +26,7 @@ (define-module (gnu tests docker) > #:use-module (gnu system image) > #:use-module (gnu system vm) > #:use-module (gnu services) > + #:use-module (gnu services containers) > #:use-module (gnu services dbus) > #:use-module (gnu services networking) > #:use-module (gnu services docker) > @@ -57,6 +58,40 @@ (define %docker-os > (service containerd-service-type) > (service docker-service-type))) > > +(define %docker-tarball > + (lower-oci-image-state > + "guile-guest" > + (packages->manifest > + (list > + guile-3.0 guile-json-3 > + (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))) Ditto. Otherwise LGTM. -- Thanks, Maxim
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Fri, 16 May 2025 02:03:04 GMT) Full text and rfc822 format available.Message #200 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: Giacomo Leidi <goodoldpaul <at> autistici.org> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 6/7] services: oci: Migrate oci-configuration to (guix records). Date: Fri, 16 May 2025 11:02:34 +0900
Hi, Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > This commit migrates oci-configuration to (guix records) singe it > appears (for-home (oci-configuration ...)) does not work as expected > with (gnu services configuration). This is supposed to be completely > transparent for users and can be reverted in the > future once this has been implemented. > > * gnu/service/containers.scm: Migrate oci-configuration to (guix records). I'd like to understand and resolve this issue and avoid pushing this commit. What is the errors you are seeing? It at least byte-compiles fine with this commit reverted and the following diff applied: --8<---------------cut here---------------start------------->8--- modified gnu/home/services/containers.scm @@ -32,16 +32,14 @@ (define home-oci-service-type (extensions (list (service-extension home-profile-service-type - (oci-service-extension-wrap-validate - (lambda (config) - (let ((runtime-cli - (oci-configuration-runtime-cli config)) - (runtime - (oci-configuration-runtime config))) - (oci-service-profile runtime runtime-cli))))) + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) (service-extension home-shepherd-service-type - (oci-service-extension-wrap-validate - oci-configuration->shepherd-services)))) + oci-configuration->shepherd-services))) (extend (lambda (config extension) (for-home --8<---------------cut here---------------end--------------->8--- Also, there are other home services using a define-configuration'd record with for-home, such as the home-snuik-service-type, so it should be possible to make it work. -- Thanks, Maxim
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Fri, 16 May 2025 02:08:02 GMT) Full text and rfc822 format available.Message #203 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: Giacomo Leidi <goodoldpaul <at> autistici.org> Cc: Tanguy Le Carrour <tanguy <at> bioneland.org>, Ludovic Courtès <ludo <at> gnu.org>, Gabriel Wicki <gabriel <at> erlikon.ch>, Andrew Tropin <andrew <at> trop.in>, Hilton Chain <hako <at> ultrarare.space>, 76081 <at> debbugs.gnu.org, Janneke Nieuwenhuizen <janneke <at> gnu.org> Subject: Re: [bug#76081] [PATCH v10 7/7] home: Add home-oci-service-type. Date: Fri, 16 May 2025 11:07:46 +0900
Hi, Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > * gnu/home/service/containers.scm: New file; > * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. > * doc/guix.texi (OCI backed services): Document it. LGTM, but please break long lines so they fit in 80 columns. This shouldn't be merged until 6/7 has been understood; if it can be eliminated this will need to be adjusted like: --8<---------------cut here---------------start------------->8--- modified gnu/home/services/containers.scm @@ -32,16 +32,14 @@ (define home-oci-service-type (extensions (list (service-extension home-profile-service-type - (oci-service-extension-wrap-validate - (lambda (config) - (let ((runtime-cli - (oci-configuration-runtime-cli config)) - (runtime - (oci-configuration-runtime config))) - (oci-service-profile runtime runtime-cli))))) + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) (service-extension home-shepherd-service-type - (oci-service-extension-wrap-validate - oci-configuration->shepherd-services)))) + oci-configuration->shepherd-services))) (extend (lambda (config extension) (for-home --8<---------------cut here---------------end--------------->8--- -- Thanks, Maxim
Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
to control <at> debbugs.gnu.org
.
(Fri, 16 May 2025 02:09:02 GMT) Full text and rfc822 format available.guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sat, 17 May 2025 15:27:02 GMT) Full text and rfc822 format available.Message #208 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 3/7] tests: oci-container: Set explicit timeouts. Date: Sat, 17 May 2025 17:26:11 +0200
[Message part 1 (text/plain, inline)]
Hi Maxim, thank you for your feedback, I should have addressed all your comments. On 5/15/25 09:21, Maxim Cournoyer wrote: >> + (let loop ((attempts 0)) >> + (if (= attempts 60) >> + (error "Service didn't come up after more than 60 seconds") > I'm curious as to why this is necessary, given in the previous test we > wait for the docker-guile service to be ready? > I think it is because between once the docker run command is run and the container is actually started and available through docker ps, there seems to be a delay. If the system is not overloaded it is minimal, but it happened to me that similarly to what happened in issue 72740 sometimes container are slow to start. I think the core problem is that the Shepherd services goes to early in the running state, I should make it stay in starting state until the container is actually started but I don't know how. If you have any pointers on how to achieve this I would be very happy to learn more. I would address it in a separate issue if you agree. It happened to me that running the test suite while compiling other guix packages could make the test fail, so I think if we keep this patch we could be safer against flaky tests. What do you think? thank you, cheers, giacomo
[Message part 2 (text/html, inline)]
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Tue, 20 May 2025 09:24:03 GMT) Full text and rfc822 format available.Message #211 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> To: paul <goodoldpaul <at> autistici.org> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 3/7] tests: oci-container: Set explicit timeouts. Date: Tue, 20 May 2025 18:23:29 +0900
Hi Paul, paul <goodoldpaul <at> autistici.org> writes: > Hi Maxim, > > thank you for your feedback, I should have addressed all your comments. > > On 5/15/25 09:21, Maxim Cournoyer wrote: >>> + (let loop ((attempts 0)) >>> + (if (= attempts 60) >>> + (error "Service didn't come up after more than 60 seconds") >> I'm curious as to why this is necessary, given in the previous test we >> wait for the docker-guile service to be ready? >> > I think it is because between once the docker run command is run and > the container is actually started and available through docker ps, > there seems to be a delay. If the system is not overloaded it is > minimal, but it happened to me that similarly to what happened in > issue 72740 sometimes container are slow to start. > > I think the core problem is that the Shepherd services goes to early > in the running state, I should make it stay in starting state until > the container is actually started but I don't know how. If you have > any pointers on how to achieve this I would be very happy to learn > more. I would address it in a separate issue if you agree. > > It happened to me that running the test suite while compiling other > guix packages could make the test fail, so I think if we keep this > patch we could be safer against flaky tests. What do you think? Thanks for explaining; yes, it seems reasonable to retry for now, and it'd be nice if you could address the root cause in the future. -- Thanks, Maxim
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 00:19:02 GMT) Full text and rfc822 format available.Message #214 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> Cc: Ludovic Courtès <ludo <at> gnu.org>, Gabriel Wicki <gabriel <at> erlikon.ch>, 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 4/7] services: Add oci-service-type. Date: Sun, 1 Jun 2025 02:18:16 +0200
[Message part 1 (text/plain, inline)]
Hi, On 5/16/25 03:38, Maxim Cournoyer wrote: >> @lisp >> -(service oci-container-service-type >> - (list >> - (oci-container-configuration >> - (network "host") >> - (image >> - (oci-image >> - (repository "guile") >> - (tag "3") >> - (value (specifications->manifest '("guile"))) >> - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) >> - #:max-layers 2)))) >> - (entrypoint "/bin/guile") >> - (command >> - '("-c" "(display \"hello!\n\")"))) >> - (oci-container-configuration >> - (image "prom/prometheus") >> - (ports >> - '(("9000" . "9000") >> - ("9090" . "9090")))) >> - (oci-container-configuration >> - (image "grafana/grafana:10.0.1") >> - (network "host") >> - (volumes >> - '("/var/lib/grafana:/var/lib/grafana"))))) >> +(simple-service 'oci-provisioning >> + oci-service-type >> + (oci-extension >> + (networks >> + (list >> + (oci-network-configuration (name "monitoring")))) >> + (containers >> + (list >> + (oci-container-configuration >> + (network "monitoring") >> + (image >> + (oci-image >> + (repository "guile") >> + (tag "3") >> + (value (specifications->manifest '("guile"))) >> + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) >> + #:max-layers 2)))) >> + (entrypoint "/bin/guile") >> + (command >> + '("-c" "(display \"hello!\n\")"))) >> + (oci-container-configuration >> + (image "prom/prometheus") >> + (network "host") >> + (ports >> + '(("9000" . "9000") >> + ("9090" . "9090")))) >> + (oci-container-configuration >> + (image "grafana/grafana:10.0.1") >> + (network "host") >> + (volumes >> + '("/var/lib/grafana:/var/lib/grafana"))))))) > I was curious: was is the gain to have by using the extension mechanism > instead of directly accepting a list of 'oci-configuration' or similarly > named record to the oci-service-type? One can always pass a list of 'oci-{container,network,volume}-configuration's to an 'oci-configuration': (service oci-service-type (oci-configuration (runtime 'podman) (containers (list (oci-container-configuration ...))))) but you loose the deduplication checks done by the extension mechanisms. This is useful especially for containers: both System/Home services and users of plain 'simple-service' in the system configuration can add Shepherd services for different reasons, if users do not set an explicit value to the 'provision' field of 'oci-container-configuration', the Shepherd service name will be derived from the image. So the 'oci-extension' suggestion in the documentation is to take care of duplicates. >> +(define (oci-object-exists? runtime runtime-cli object verbose?) >> + #~(lambda* (name #:key (format-string "{{.Name}}")) >> + (use-modules (ice-9 format) >> + (ice-9 match) >> + (ice-9 popen) >> + (ice-9 rdelim) >> + (srfi srfi-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))) >> + >> + #$(if (eq? runtime 'podman) >> + #~(let ((command >> + (list #$runtime-cli >> + #$object "exists" name))) >> + (when #$verbose? >> + (format #t "Running~{ ~a~}~%" command)) >> + (define exit-code (status:exit-val (apply system* command))) >> + (when #$verbose? >> + (format #t "Exit code: ~a~%" exit-code)) >> + (equal? EXIT_SUCCESS exit-code)) >> + #~(let ((command >> + (string-append #$runtime-cli >> + " " #$object " ls --format " >> + "\"" format-string "\""))) >> + (when #$verbose? >> + (format #t "Running ~a~%" command)) >> + (member name (read-lines (open-input-pipe command))))))) > That's a complex piece of staged code; have you considered placing it in > a supporting build-side module, e.g. gnu/build/oci-containers.scm ? > Then it could be more simply imported in the gexp environment and used. > See the gnu/build/jami-service.scm for an example. Thank you I was not aware of this option, I tried to move as much as I could to gnu/build/oci-containers.scm. >> +(define* (oci-image-loader runtime runtime-cli name image tag #:key (verbose? #f)) >> + "Return a file-like object that, once lowered, will evaluate to a program able >> +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." >> + (let ((tarball (lower-oci-image name image))) >> (with-imported-modules '((guix build utils)) >> - (program-file (format #f "~a-image-loader" name) >> + (program-file >> + (format #f "~a-image-loader" name) >> #~(begin >> (use-modules (guix build utils) >> + (ice-9 match) >> (ice-9 popen) >> - (ice-9 rdelim)) >> - >> - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) >> - (define line >> - (read-line >> - (open-input-pipe >> - (string-append #$docker " load -i " #$tarball)))) >> - >> - (unless (or (eof-object? line) >> - (string-null? line)) >> - (format #t "~a~%" line) >> - (let ((repository&tag >> - (string-drop line >> - (string-length >> - "Loaded image: ")))) >> - >> - (invoke #$docker "tag" repository&tag #$tag) >> - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) >> - >> -(define (oci-container-shepherd-service config) >> - (define (guess-name name image) >> - (if (maybe-value-set? name) >> - name >> - (string-append "docker-" >> - (basename >> - (if (string? image) >> - (first (string-split image #\:)) >> - (oci-image-repository image)))))) >> - >> - (let* ((docker (file-append docker-cli "/bin/docker")) >> - (actions (oci-container-configuration-shepherd-actions config)) >> + (ice-9 rdelim) >> + (srfi srfi-1)) >> + (define object-exists? >> + #$(oci-object-exists? runtime runtime-cli "image" verbose?)) >> + (define load-command >> + (string-append #$runtime-cli >> + " load -i " #$tarball)) >> + >> + (if (object-exists? #$tag #:format-string "{{.Repository}}:{{.Tag}}") >> + (format #t "~a image already exists, skipping.~%" #$tag) >> + (begin >> + (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) >> + (when #$verbose? >> + (format #t "Running ~a~%" load-command)) >> + (let ((line (read-line >> + (open-input-pipe load-command)))) >> + (unless (or (eof-object? line) >> + (string-null? line)) >> + (format #t "~a~%" line) >> + (let* ((repository&tag >> + (string-drop line >> + (string-length >> + "Loaded image: "))) >> + (tag-command >> + (list #$runtime-cli "tag" repository&tag #$tag)) >> + (drop-old-tag-command >> + (list #$runtime-cli "image" "rm" "-f" repository&tag))) >> + >> + (unless (string=? repository&tag #$tag) >> + (when #$verbose? >> + (format #t "Running~{ ~a~}~%" tag-command)) >> + >> + (let ((exit-code >> + (status:exit-val (apply system* tag-command)))) >> + (format #t "Tagged ~a with ~a...~%" #$tarball #$tag) >> + >> + (when #$verbose? >> + (format #t "Exit code: ~a~%" exit-code)) >> + >> + (when (equal? EXIT_SUCCESS exit-code) >> + (when #$verbose? >> + (format #t "Running~{ ~a~}~%" drop-old-tag-command)) >> + (let ((drop-exit-code >> + (status:exit-val (apply system* drop-old-tag-command)))) >> + (when #$verbose? >> + (format #t "Exit code: ~a~%" drop-exit-code)))))))))))))))) >> + >> +(define (oci-container-run-invocation runtime runtime-cli name command image-reference >> + options runtime-extra-arguments run-extra-arguments) > That's a lot of arguments! Perhaps it could accept a record instead? > Please also mind the 80 chars max width. Good idea, I made an intermediate oci-container-invocation record. >> + "Return a list representing the OCI runtime >> +invocation for running containers." >> + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] >> + `(,runtime-cli ,@runtime-extra-arguments "run" "--rm" >> + ,@(if (eq? runtime 'podman) >> + ;; This is because podman takes some time to >> + ;; release container names. --replace seems >> + ;; to be required to be able to restart services. >> + '("--replace") >> + '()) >> + "--name" ,name >> + ,@options ,@run-extra-arguments >> + ,image-reference ,@command)) >> + >> +(define* (oci-container-entrypoint runtime runtime-cli name image image-reference >> + invocation #:key (verbose? #f) (pre-script #~())) > The default value for a keyword argument is #f, so I'd omit the explicit > default for verbose?. >> + (begin >> + (when #$verbose? >> + (format #t "Running~{ ~a~}~%" invocation)) >> + (let ((exit-code (status:exit-val (apply system* invocation)))) >> + (when #$verbose? >> + (format #t "Exit code: ~a~%" exit-code)))))) >> + (list #$@invocations))))) >> + >> +(define* (oci-object-shepherd-service object runtime runtime-cli name requirement invocations > Max width :-). I should have dropped the default #f for keywords in all procedure, thanks. >> + "Return a file-like object that, once lowered, will evaluate to the entrypoint >> +for the Shepherd service that will run IMAGE through RUNTIME-CLI." >> + (program-file >> + (string-append "oci-entrypoint-" name) >> + #~(begin >> + (use-modules (ice-9 format) >> + (srfi srfi-1)) >> + (when #$verbose? >> + (format #t "Running in verbose mode...~%") >> + (format #t "Current user: ~a ~a~%" >> + (getuid) (passwd:name (getpwuid (getuid)))) >> + (format #t "Current group: ~a ~a~%" >> + (getgid) (group:name (getgrgid (getgid)))) >> + (format #t "Current directory ~a~%" (getcwd))) > I'd use a single format call; you can format the string naturally like > > (format #t "\ > Running in verbose mode... > Current user: ~a ~a > ... > " arg1 arg2, ...) > > >> + (define invocation (list #$@invocation)) >> + #$@pre-script >> + (when #$verbose? >> + (format #t "Running~{ ~a~}~%" invocation)) >> + (apply execlp `(,(first invocation) ,@invocation))))) > You can more simply: (apply execlp (first invocation) invocation) Thank you, I have addressed this in gnu/build/oci-containers.scm >> + >> +(define* (oci-container-shepherd-service runtime runtime-cli config >> + #:key >> + (runtime-environment #~()) >> + (runtime-extra-arguments '()) >> + (oci-requirement '()) >> + (user #f) >> + (group #f) >> + (verbose? #f)) >> + "Return a Shepherd service object that will run the OCI container represented >> +by CONFIG through RUNTIME-CLI." >> + (let* ((actions (oci-container-configuration-shepherd-actions config)) >> (auto-start? >> (oci-container-configuration-auto-start? config)) >> - (user (oci-container-configuration-user config)) >> - (group (oci-container-configuration-group config)) >> + (maybe-user (oci-container-configuration-user config)) >> + (maybe-group (oci-container-configuration-group config)) >> + (user (if (maybe-value-set? maybe-user) >> + maybe-user >> + user)) >> + (group (if (maybe-value-set? maybe-group) >> + maybe-group >> + group)) >> (host-environment >> (oci-container-configuration-host-environment config)) >> (command (oci-container-configuration-command config)) >> (log-file (oci-container-configuration-log-file config)) >> - (provision (oci-container-configuration-provision config)) >> (requirement (oci-container-configuration-requirement config)) >> (respawn? >> (oci-container-configuration-respawn? config)) >> (image (oci-container-configuration-image config)) >> (image-reference (oci-image-reference image)) >> (options (oci-container-configuration->options config)) >> - (name (guess-name provision image)) >> + (name >> + (oci-container-shepherd-name runtime config)) >> (extra-arguments >> - (oci-container-configuration-extra-arguments config))) >> + (oci-container-configuration-extra-arguments config)) >> + (invocation >> + (oci-container-run-invocation >> + runtime runtime-cli name command image-reference >> + options runtime-extra-arguments extra-arguments)) >> + (container-action >> + (lambda* (command #:key (environment-variables #f)) >> + #~(lambda _ >> + (fork+exec-command >> + (list #$@command) >> + #$@(if user (list #:user user) '()) >> + #$@(if group (list #:group group) '()) >> + #$@(if (maybe-value-set? log-file) >> + (list #:log-file log-file) >> + '()) >> + #$@(if (and user (eq? runtime 'podman)) >> + (list #:directory >> + #~(passwd:dir (getpwnam #$user))) >> + '()) >> + #$@(if environment-variables >> + (list #:environment-variables >> + environment-variables) >> + '())))))) > That's a very long let. Since many values appear to simply be accessed > from theh oci-container-configuration, match-record could be used to > great effect. For readability, I'd also define the action procedures > using nested 'define' inside the let/match-record. I tried to address this but it's really difficult to trim much, please let me know if there's some better way. >> +(define (oci-service-subids config) >> + "Return a subids-extension record representing subuids and subgids required by >> +the rootless Podman backend." >> + (define (delete-duplicate-ranges ranges) >> + (delete-duplicates ranges >> + (lambda args >> + (apply string=? (map subid-range-name ranges))))) > I don't understand how the above 'delete-duplicate-ranges' procedure > works; the 2nd argument of delete-duplicates is normally a predicate > that accepts two values, but here it's a lambda that disregards the > arguments: it seems it'd be wasteful to compute the same result over and > over? It is, I just replace this with a more descriptive error that allows the users to drop the duplicate without silently dropping a random one between the two. >> + (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")))) > Is there perhaps an easier way to check that the podman services were > started successfully? Perhaps using the podman command directly to > query the state of the running containers? > > 'ls' is usually frowned for scripting; it's meant for interactive use. > Perhaps you could use the shell wildcard feature or the find command > instead? I comment explaining what the above does/check exactly would > be useful; it's not obvious. I think this test can be dropped anyway since we later wait for Shepherd services to be started, it is a development leftover. >> + (let loop ((attempts 0)) >> + (if (= attempts 60) >> + (error "Services didn't come up after more than 60 seconds") >> + (if (equal? '("cgroup" "cgroup") >> + (run-test)) >> + #t >> + (begin >> + (sleep 1) >> + (format #t "Services didn't come up yet, retrying with attempt ~a~%" >> + (+ 1 attempts)) >> + (loop (+ 1 attempts)))))))) > Note that we have a 'with-retries' definition in (gnu build > jami-service) which could be used here; perhaps it should be relocated > elsewhere. Thank you for the hint, I created gnu/build/utils.scm and tested my changes with: guix shell -CPWN --writable-root nss-certs -m manifest.scm -- make check-system TESTS="pounce jami ngircd" >> + (lambda (port) >> + (display (string-join response "\n") port))))) >> + (lambda () >> + (primitive-exit 127)))) > The fork is used to be able to run the podman command with the right > passwd and uid, right? Maybe you could use the shepherd's provided > 'command' or 'fork+exec-command', which have a nice API to do this. > Also, is the dynamic-wind unnecessary? I would have thought that if a > forked children aborts, its exit status should be reflected accordingly? You bring two good points, addressed by dropping dynamic-wind and refactoring duplicate code in tests. > The idiom of forking ot invoke podman is used so commonly it should be > extracted to a procedure or syntax and reused. This should be addressed now. > Phew, that was a large chunk! I'll continue my review with the > remaining commits, but when I'm done, could you please submit a v11, > taking into account my comments? Thank you a lot for taking the time to go through all of this, I hope I addressed all your comments successfully. Please let me know if something seems off. I'll send a v11 soon. cheers, giacomo
[Message part 2 (text/html, inline)]
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 01:43:05 GMT) Full text and rfc822 format available.Message #217 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 5/7] tests: Use lower-oci-image-state in container tests. Date: Sun, 1 Jun 2025 03:42:19 +0200
Hi, On 5/16/25 03:45, Maxim Cournoyer wrote: > Hi, > > Giacomo Leidi <goodoldpaul <at> autistici.org> writes: > >> This patch replaces boilerplate in container related tests with >> oci-image plumbing from (gnu services containers). >> >> * gnu/services/containers.scm: Export lower-oci-image-state. > I don't think that's a nice API to export; you can instead use the > > (define lower-oci-image-state > (@@ (gnu services containers) lower-oci-image-state) > > trick to expose internals (white box testing). Good point, addressed. >> * gnu/tests/containers.scm (%oci-tarball): New variable; >> (run-rootless-podman-test): use %oci-tarball; >> (build-tarball&run-rootless-podman-test): drop procedure. >> * gnu/tests/docker.scm (%docker-tarball): New variable; >> (build-tarball&run-docker-test): use %docker-tarball; >> (%docker-system-tarball): New variable; >> (build-tarball&run-docker-system-test): new procedure. >> >> Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a >> --- >> gnu/services/containers.scm | 2 + >> gnu/tests/containers.scm | 80 ++++++++++++++--------------- >> gnu/tests/docker.scm | 100 ++++++++++++++++++++---------------- >> 3 files changed, 95 insertions(+), 87 deletions(-) > The diffstats suggest the result is not that impactful in terms of > reducing boilerplate, ah! I would like to work to a single Scheme API in the future, that can be common to guix pack, guix system internals and the oci-service-type internals. But yeah the boilerplate is not much less. v11 should have all your comments addressed, thank you for your review. cheers, giacomo
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 02:25:05 GMT) Full text and rfc822 format available.Message #220 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> Cc: 76081 <at> debbugs.gnu.org Subject: Re: [bug#76081] [PATCH v10 6/7] services: oci: Migrate oci-configuration to (guix records). Date: Sun, 1 Jun 2025 04:24:38 +0200
[Message part 1 (text/plain, inline)]
Hi, On 5/16/25 04:02, Maxim Cournoyer wrote: > Also, there are other home services using a define-configuration'd > record with for-home, such as the home-snuik-service-type, so it should > be possible to make it work. You are right, thank you! I was probably mistaken. I dropped the patch. cheers, giacomo
[Message part 2 (text/html, inline)]
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 02:50:05 GMT) Full text and rfc822 format available.Message #223 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: paul <goodoldpaul <at> autistici.org> To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com> Cc: Tanguy Le Carrour <tanguy <at> bioneland.org>, Ludovic Courtès <ludo <at> gnu.org>, Gabriel Wicki <gabriel <at> erlikon.ch>, Andrew Tropin <andrew <at> trop.in>, Hilton Chain <hako <at> ultrarare.space>, 76081 <at> debbugs.gnu.org, Janneke Nieuwenhuizen <janneke <at> gnu.org> Subject: Re: [bug#76081] [PATCH v10 7/7] home: Add home-oci-service-type. Date: Sun, 1 Jun 2025 04:49:33 +0200
[Message part 1 (text/plain, inline)]
Hi Maxim, thank you for your extensive help. I'm sending a v11 which hopefully addresses everything. I tested my changes with: guix shell -CPWN --writable-root nss-certs -m manifest.scm -- make check-system TESTS="pounce jami ngircd oci-service-rootless-podman oci-service-docker oci-container rootless-podman docker docker-system" cheers, giacomo
[Message part 2 (text/html, inline)]
gabriel <at> erlikon.ch, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 02:52:06 GMT) Full text and rfc822 format available.Message #226 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v11 3/5] services: Add oci-service-type. Date: Sun, 1 Jun 2025 04:51:16 +0200
This patch implements a generalization of the oci-container-service-type, which consequently is made deprecated. The oci-service-type, in addition to all the features from the oci-container-service-type, can now provision OCI networks and volumes. It only handles OCI objects creation, the user is supposed to handle state once the objects are provsioned. It currently supports two different OCI runtimes: Docker and rootless Podman. Both runtimes are tested to make sure provisioned containers can connect to each other through provisioned networks and can read/write data with provisioned volumes. At last the Scheme API is thought to facilitate the implementation of a Guix Home service in the future. * gnu/build/oci-containers.scm: New file containg OCI runtime business logic used in OCI backed Shepherd services. oci-read-lines (oci-system*,oci-object-exists?,oci-object-service-available? oci-image-load,oci-log-verbose,oci-container-execlp,oci-object-create): New procedures. * gnu/local.mk: Add it. * gnu/services/containers.scm (list-of-oci-containers?, list-of-oci-networks?,list-of-oci-volumes?,%oci-supported-runtimes, oci-runtime?,oci-runtime-system-environment,oci-runtime-system-extra-arguments, oci-runtime-system-requirement,oci-runtime-cli,oci-runtime-system-cli, oci-runtime-home-cli,oci-runtime-name,oci-runtime-group, oci-container-shepherd-name,oci-networks-shepherd-name, oci-networks-home-shepherd-name,oci-volumes-shepherd-name, oci-volumes-home-shepherd-name,oci-container-configuration->options, oci-network-configuration->options,oci-volume-configuration->options, oci-container-shepherd-service,oci-objects-merge-lst,oci-extension-merge, oci-service-accounts,oci-service-profile,oci-service-subids, oci-configuration->shepherd-services,oci-configuration-extend): New procedures. (image-reference): Implement unambiguous naming convention, that paired with the new implementation for listing caches images with docker ls or podman ls, allows for more efficient image caching. (oci-container-configuration)[user,group]: Change default-type to maybe-string, since by default containers will run under the user and group declared in oci-configuration records. When unset the oci-service-type will derive their value from the OCI runtime state. [runtime,host-environment,environment,shepherd-actions,ports,extra-arguments]: define a predicate and use it as a type in the configuration. This way errors are reported with source location information. (lower-manifest): Defer to caller the logic of setting up an image tag. (lower-oci-image): Rename to load-oci-image-state. (oci-runtime-state): Intermediate representation of the OCI runtime details. It is supposed to be an internal API. (oci-state): Intermediate representation of the OCI provisioning state, such as containers and networks. It is supposed to be an internal API. (oci-container-invocation): Intermediate representation of the OCI runtime run command to start a container. It is supposed to be an internal API. (%oci-image-loader): Rename to oci-image-loader and use oci-runtime-state and (gnu build oci-containers). (oci-container-shepherd-service): Use oci-state and oci-runtime-state, add command-line action. (oci-network-configuration,oci-volume-configuration,oci-configuration, oci-extension): New record types. (oci-service-type): New service-type. * doc/guix.texi: Document it. * gnu/tests/containers.scm: Test it. * gnu/services/docker.scm: Deprecate the oci-container-service-type. Change-Id: I656b3db85832e42d53072fcbfb91d1226f39ef38 Signed-off-by: Giacomo Leidi <goodoldpaul <at> autistici.org> --- doc/guix.texi | 306 ++++++-- gnu/build/oci-containers.scm | 210 ++++++ gnu/local.mk | 1 + gnu/services/containers.scm | 1365 +++++++++++++++++++++++++++++----- gnu/services/docker.scm | 38 +- gnu/tests/containers.scm | 561 +++++++++++++- 6 files changed, 2208 insertions(+), 273 deletions(-) create mode 100644 gnu/build/oci-containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index 5fefba36342..ae49d0c547b 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -43937,59 +43937,162 @@ Miscellaneous Services @cindex OCI-backed, Shepherd services @subsubheading OCI backed services -Should you wish to manage your Docker containers with the same consistent -interface you use for your other Shepherd services, -@var{oci-container-service-type} is the tool to use: given an -@acronym{Open Container Initiative, OCI} container image, it will run it in a +Should you wish to manage your @acronym{Open Container Initiative, OCI} containers +with the same consistent interface you use for your other Shepherd services, +@var{oci-service-type} is the tool to use: given an +OCI container image, it will run it in a Shepherd service. One example where this is useful: it lets you run services -that are available as Docker/OCI images but not yet packaged for Guix. +that are available as OCI images but not yet packaged for Guix. -@defvar oci-container-service-type +@defvar oci-service-type -This is a thin wrapper around Docker's CLI that executes OCI images backed +This is a thin wrapper around Docker's or Podman's CLI that executes OCI images backed processes as Shepherd Services. @lisp -(service oci-container-service-type - (list - (oci-container-configuration - (network "host") - (image - (oci-image - (repository "guile") - (tag "3") - (value (specifications->manifest '("guile"))) - (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) - #:max-layers 2)))) - (entrypoint "/bin/guile") - (command - '("-c" "(display \"hello!\n\")"))) - (oci-container-configuration - (image "prom/prometheus") - (ports - '(("9000" . "9000") - ("9090" . "9090")))) - (oci-container-configuration - (image "grafana/grafana:10.0.1") - (network "host") - (volumes - '("/var/lib/grafana:/var/lib/grafana"))))) +(simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "host") + (ports + '(("9000" . "9000") + ("9090" . "9090")))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "host") + (volumes + '("/var/lib/grafana:/var/lib/grafana"))))))) @end lisp In this example three different Shepherd services are going to be added to the system. Each @code{oci-container-configuration} record translates to a -@code{docker run} invocation and its fields directly map to options. You can -refer to the -@url{https://docs.docker.com/engine/reference/commandline/run,upstream} -documentation for the semantics of each value. If the images are not found, -they will be -@url{https://docs.docker.com/engine/reference/commandline/pull/,pulled}. The +@samp{docker run} or @samp{podman run} invocation and its fields directly +map to options. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html,Podman} +upstream documentation for semantics of each value. If the images are not found, +they will be pulled. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/pull/,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-pull.1.html,Podman} +upstream documentation for semantics. The services with @code{(network "host")} are going to be attached to the host network and are supposed to behave like native processes with regard to networking. @end defvar +@c %start of fragment + +@deftp {Data Type} oci-configuration +Available @code{oci-configuration} fields are: + +@table @asis +@item @code{runtime} (default: @code{'docker}) (type: symbol) +The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}. + +@item @code{runtime-cli} (type: maybe-package-or-string) +The OCI runtime command line to be installed in the system profile and used +to provision OCI resources, it can be either a package or a string representing +an absolute file name to the runtime binary entrypoint. When unset it will default +to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} +package for the @code{'podman} runtime. + +@item @code{runtime-extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation. + +@item @code{user} (type: maybe-string) +The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}. + +@item @code{group} (type: maybe-string) +The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}. + +@item @code{subuids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{subgids-range} (type: maybe-subid-range) +An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name "oci-container"))}. + +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. The use of the @code{oci-extension} record should +be preferred for most cases. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. The use of the @code{oci-extension} record should +be preferred for most cases. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. The use of the @code{oci-extension} record should +be preferred for most cases. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +When true, additional output will be printed, allowing to better follow the +flow of execution. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-extension +Available @code{oci-extension} fields are: + +@table @asis +@item @code{containers} (default: @code{'()}) (type: list-of-oci-containers) +The list of @code{oci-container-configuration} records representing the +containers to provision. + +@item @code{networks} (default: @code{'()}) (type: list-of-oci-networks) +The list of @code{oci-network-configuration} records representing the +containers to provision. + +@item @code{volumes} (default: @code{'()}) (type: list-of-oci-volumes) +The list of @code{oci-volumes-configuration} records representing the +containers to provision. + +@end table + +@end deftp + + +@c %end of fragment + + @c %start of fragment @deftp {Data Type} oci-container-configuration @@ -44009,16 +44112,16 @@ Miscellaneous Services Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image. @item @code{host-environment} (default: @code{'()}) (type: list) -Set environment variables in the host environment where @command{docker -run} is invoked. This is especially useful to pass secrets from the -host to the container without having them on the @command{docker run}'s -command line: by setting the @code{MYSQL_PASSWORD} on the host and by passing +Set environment variables in the host environment where @samp{docker run} +or @samp{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @lisp -(list '("LANGUAGE\" . "eo:ca:eu") +(list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp @@ -44026,22 +44129,24 @@ Miscellaneous Services directly to @code{make-forkexec-constructor}. @item @code{environment} (default: @code{'()}) (type: list) -Set environment variables. This can be a list of pairs or strings, even mixed: +Set environment variables inside the container. This can be a list of pairs +or strings, even mixed: @lisp (list '("LANGUAGE" . "eo:ca:eu") "JAVA_HOME=/opt/java") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics. @item @code{image} (type: string-or-oci-image) The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker Engine, and -follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}. @item @code{provision} (default: @code{""}) (type: string) @@ -44069,7 +44174,7 @@ Miscellaneous Services by the service. @item @code{network} (default: @code{""}) (type: string) -Set a Docker network for the spawned container. +Set an OCI network for the spawned container. @item @code{ports} (default: @code{'()}) (type: list) Set the port or port ranges to expose from the spawned container. This can be a @@ -44080,10 +44185,11 @@ Miscellaneous Services "10443:443") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics. @item @code{volumes} (default: @code{'()}) (type: list) Set volume mappings for the spawned container. This can be a @@ -44094,25 +44200,97 @@ Miscellaneous Services "/gnu/store:/gnu/store") @end lisp -Pair members can be strings, gexps or file-like objects. -Strings are passed directly to the Docker CLI. You can refer to the -@uref{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics. +Pair members can be strings, gexps or file-like objects. Strings are passed +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics. @item @code{container-user} (default: @code{""}) (type: string) Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics. @item @code{workdir} (default: @code{""}) (type: string) Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics. +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @samp{docker run} or @samp{podman run} invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-network-configuration +Available @code{oci-network-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI network to provision. + +@item @code{driver} (type: maybe-string) +The driver to manage the network. + +@item @code{gateway} (type: maybe-string) +IPv4 or IPv6 gateway for the subnet. + +@item @code{internal?} (default: @code{#f}) (type: boolean) +Restrict external access to the network + +@item @code{ip-range} (type: maybe-string) +Allocate container ip from a sub-range in CIDR format. + +@item @code{ipam-driver} (type: maybe-string) +IP Address Management Driver. + +@item @code{ipv6?} (default: @code{#f}) (type: boolean) +Enable IPv6 networking. + +@item @code{subnet} (type: maybe-string) +Subnet in CIDR format that represents a network segment. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. + +@item @code{extra-arguments} (default: @code{'()}) (type: list) +A list of strings, gexps or file-like objects that will be directly passed +to the @samp{docker network create} or @samp{podman network create} +invokation. + +@end table + +@end deftp + + +@c %end of fragment + +@c %start of fragment + +@deftp {Data Type} oci-volume-configuration +Available @code{oci-volume-configuration} fields are: + +@table @asis +@item @code{name} (type: string) +The name of the OCI volume to provision. + +@item @code{labels} (default: @code{'()}) (type: list) +The list of labels that will be used to tag the current volume. @item @code{extra-arguments} (default: @code{'()}) (type: list) -A list of strings, gexps or file-like objects that will be directly -passed to the @command{docker run} invocation. +A list of strings, gexps or file-like objects that will be directly passed +to the @samp{docker volume create} or @samp{podman volume create} +invokation. @end table diff --git a/gnu/build/oci-containers.scm b/gnu/build/oci-containers.scm new file mode 100644 index 00000000000..38704e9e4a4 --- /dev/null +++ b/gnu/build/oci-containers.scm @@ -0,0 +1,210 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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/>. + +;;; Commentary: +;;; +;;; This module contains helpers used as part of the oci-service-type +;;; definition. +;;; +;;; Code: + +(define-module (gnu build oci-containers) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (ice-9 popen) + #:use-module (ice-9 rdelim) + #:use-module (ice-9 textual-ports) + #:use-module (srfi srfi-1) + #:export (oci-read-lines + oci-system* + oci-object-exists? + oci-object-service-available? + oci-image-load + oci-log-verbose + oci-container-execlp + oci-object-create)) + +(define* (oci-read-lines invocation #:key verbose?) + (define (get-lines port) + (let ((lines-string (get-string-all port))) + (string-split lines-string #\newline))) + + (define command + (string-join invocation " ")) + + (when verbose? (format #t "Running ~a~%" command)) + + (with-input-from-port (open-input-pipe command) + (lambda _ + (get-lines (current-input-port))))) + +(define* (oci-log-verbose invocation) + (format #t "Running in verbose mode... +Current user: ~a ~a +Current group: ~a ~a +Current directory: ~a~%" + (getuid) (passwd:name (getpwuid (getuid))) + (getgid) (group:name (getgrgid (getgid))) + (getcwd)) + + (format #t "Running~{ ~a~}~%" invocation)) + +(define* (oci-system* invocation #:key verbose?) + (when verbose? + (format #t "Running~{ ~a~}~%" invocation)) + + (let* ((status (apply system* invocation)) + (exit-code (status:exit-val status))) + (when verbose? + (format #t "Exit code: ~a~%" exit-code)) + status)) + +(define* (oci-object-member name objects + #:key verbose?) + + (define member? (member name objects)) + + (when (and verbose? (> (length objects) 0)) + (format #t "~a is ~apart of:~{ ~a~}~%" + name + (if member? "" "not ") + objects)) + member?) + +(define* (oci-object-list runtime-cli object + #:key verbose? + (format-string "{{.Name}}")) + + (define invocation + (list runtime-cli object "ls" "--format" + (string-append "\"" format-string "\""))) + + (filter + (lambda (name) + (not (string=? (string-trim name) ""))) + (oci-read-lines invocation #:verbose? verbose?))) + +(define* (docker-object-exist? runtime-cli object name + #:key verbose? + (format-string "{{.Name}}")) + + (define objects + (oci-object-list runtime-cli object + #:verbose? verbose? + #:format-string format-string)) + + (oci-object-member name objects #:verbose? verbose?)) + +(define* (podman-object-exist? runtime-cli object name #:key verbose?) + (let ((invocation (list runtime-cli object "exists" name))) + (define exit-code + (status:exit-val (oci-system* invocation #:verbose? verbose?))) + (equal? EXIT_SUCCESS exit-code))) + +(define* (oci-object-exists? runtime runtime-cli object name + #:key verbose? + (format-string "{{.Name}}")) + (if (eq? runtime 'podman) + (podman-object-exist? runtime-cli object name + #:verbose? verbose?) + (docker-object-exist? runtime-cli object name + #:verbose? verbose? + #:format-string format-string))) + +(define* (oci-object-service-available? runtime-cli object names + #:key verbose? + (format-string "{{.Name}}")) + "Whether NAMES are provisioned in the current OBJECT environment." + (define environment + (oci-object-list runtime-cli object + #:verbose? verbose? + #:format-string format-string)) + (when verbose? + (format #t "~a environment:~{ ~a~}~%" object environment)) + + (define available? + (every + (lambda (name) + (oci-object-member name environment #:verbose? verbose?)) + names)) + + (when verbose? + (format #t "~a service is~a available~%" object (if available? "" " not"))) + + available?) + +(define* (oci-image-load runtime runtime-cli tarball name tag + #:key verbose? + (format-string "{{.Repository}}:{{.Tag}}")) + (define load-invocation + (list runtime-cli "load" "-i" tarball)) + + (if (oci-object-exists? runtime runtime-cli "image" tag + #:verbose? verbose? + #:format-string format-string) + (format #t "~a image already exists, skipping.~%" tag) + (begin + (format #t "Loading image for ~a from ~a...~%" name tarball) + + (let ((line (first + (oci-read-lines load-invocation #:verbose? verbose?)))) + (unless (or (eof-object? line) + (string-null? line)) + + (format #t "~a~%" line) + + (let* ((repository&tag + (string-drop line + (string-length + "Loaded image: "))) + (tag-invocation + (list runtime-cli "tag" repository&tag tag)) + (drop-old-tag-invocation + (list runtime-cli "image" "rm" "-f" repository&tag))) + + (unless (string=? repository&tag tag) + (let ((exit-code + (status:exit-val + (oci-system* tag-invocation #:verbose? verbose?)))) + (format #t "Tagged ~a with ~a...~%" tarball tag) + + (when (equal? EXIT_SUCCESS exit-code) + (oci-system* drop-old-tag-invocation #:verbose? verbose?)))))))))) + +(define* (oci-container-execlp invocation #:key verbose? pre-script) + (when pre-script + (pre-script)) + (when verbose? + (oci-log-verbose invocation)) + (apply execlp (first invocation) invocation)) + +(define* (oci-object-create runtime runtime-cli runtime-name + object + invocations + #:key verbose? + (format-string "{{.Name}}")) + (for-each + (lambda (invocation) + (define name (last invocation)) + (if (oci-object-exists? runtime runtime-cli object name + #:format-string format-string + #:verbose? verbose?) + (format #t "~a ~a ~a already exists, skipping creation.~%" + runtime-name name object) + (oci-system* invocation #:verbose? verbose?))) + invocations)) diff --git a/gnu/local.mk b/gnu/local.mk index 4500bfc6d33..09ccd64b40a 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -840,6 +840,7 @@ GNU_SYSTEM_MODULES = \ %D%/build/linux-initrd.scm \ %D%/build/linux-modules.scm \ %D%/build/marionette.scm \ + %D%/build/oci-containers.scm \ %D%/build/secret-service.scm \ %D%/build/utils.scm \ \ diff --git a/gnu/services/containers.scm b/gnu/services/containers.scm index 24f31c756b8..745a40ff02b 100644 --- a/gnu/services/containers.scm +++ b/gnu/services/containers.scm @@ -35,12 +35,15 @@ (define-module (gnu services containers) #:use-module (guix diagnostics) #:use-module (guix gexp) #:use-module (guix i18n) + #:use-module (guix modules) #:use-module (guix monads) #:use-module (guix packages) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) + #:use-module (guix records) #:use-module (guix store) #:use-module (srfi srfi-1) + #:use-module (ice-9 format) #:use-module (ice-9 match) #:export (rootless-podman-configuration rootless-podman-configuration? @@ -96,8 +99,82 @@ (define-module (gnu services containers) oci-container-configuration-workdir oci-container-configuration-extra-arguments + list-of-oci-containers? + list-of-oci-networks? + list-of-oci-volumes? + + %oci-supported-runtimes + oci-runtime? + oci-runtime-system-environment + oci-runtime-system-extra-arguments + oci-runtime-system-requirement + oci-runtime-cli + oci-runtime-system-cli + oci-runtime-home-cli + oci-runtime-name + oci-runtime-group + + oci-network-configuration + oci-network-configuration? + oci-network-configuration-fields + oci-network-configuration-name + oci-network-configuration-driver + oci-network-configuration-gateway + oci-network-configuration-internal? + oci-network-configuration-ip-range + oci-network-configuration-ipam-driver + oci-network-configuration-ipv6? + oci-network-configuration-subnet + oci-network-configuration-labels + oci-network-configuration-extra-arguments + + oci-volume-configuration + oci-volume-configuration? + oci-volume-configuration-fields + oci-volume-configuration-name + oci-volume-configuration-labels + oci-volume-configuration-extra-arguments + + oci-configuration + oci-configuration? + oci-configuration-fields + oci-configuration-runtime + oci-configuration-runtime-cli + oci-configuration-runtime-extra-arguments + oci-configuration-user + oci-configuration-group + oci-configuration-containers + oci-configuration-networks + oci-configuration-volumes + oci-configuration-verbose? + oci-configuration-valid? + + oci-extension + oci-extension? + oci-extension-fields + oci-extension-containers + oci-extension-networks + oci-extension-volumes + + oci-container-shepherd-name + oci-networks-shepherd-name + oci-networks-home-shepherd-name + oci-volumes-shepherd-name + oci-volumes-home-shepherd-name + + oci-container-configuration->options + oci-network-configuration->options + oci-volume-configuration->options + oci-container-shepherd-service - %oci-container-accounts)) + oci-objects-merge-lst + oci-extension-merge + oci-service-type + oci-service-accounts + oci-service-profile + oci-service-subids + oci-configuration->shepherd-services + oci-configuration-extend)) (define (gexp-or-string? value) (or (gexp? value) @@ -296,9 +373,42 @@ (define rootless-podman-service-type ;;; -;;; OCI container. +;;; OCI provisioning service. ;;; +(define %oci-supported-runtimes + '(docker podman)) + +(define (oci-runtime-system-requirement runtime) + "Return a list of Shepherd service names required by a given OCI runtime, +before it is able to run containers." + (if (eq? 'podman runtime) + '(cgroups2-fs-owner cgroups2-limits + rootless-podman-shared-root-fs user-processes) + '(dockerd user-processes))) + +(define (oci-runtime-name runtime) + "Return a human readable name for a given OCI runtime." + (if (eq? 'podman runtime) + "Podman" "Docker")) + +(define (oci-runtime-group runtime maybe-group) + "Implement the logic behind selection of the group that is to be used by +Shepherd to execute OCI commands." + (if (maybe-value-set? maybe-group) + maybe-group + (if (eq? 'podman runtime) + "cgroup" + "docker"))) + +(define (oci-runtime? value) + (unless (member value %oci-supported-runtimes) + (raise + (formatted-message + (G_ "OCI runtime must be a symbol and one of ~a, +but ~a was found") %oci-supported-runtimes value))) + (symbol? value)) + (define (oci-sanitize-pair pair delimiter) (define (valid? member) (or (string? member) @@ -332,21 +442,41 @@ (define (oci-sanitize-host-environment value) ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") (oci-sanitize-mixed-list "host-environment" value "=")) +(define (oci-container-host-environment? value) + (list? (oci-sanitize-host-environment value))) + (define (oci-sanitize-environment value) ;; Expected spec format: ;; '(("HOME" . "/home/nobody") "JAVA_HOME=/java") (oci-sanitize-mixed-list "environment" value "=")) +(define (oci-container-environment? value) + (list? (oci-sanitize-environment value))) + (define (oci-sanitize-ports value) ;; Expected spec format: ;; '(("8088" . "80") "2022:22") (oci-sanitize-mixed-list "ports" value ":")) +(define (oci-container-ports? value) + (list? (oci-sanitize-ports value))) + (define (oci-sanitize-volumes value) ;; Expected spec format: ;; '(("/mnt/dir" . "/dir") "/run/current-system/profile:/java") (oci-sanitize-mixed-list "volumes" value ":")) +(define (oci-container-volumes? value) + (list? (oci-sanitize-volumes value))) + +(define (oci-sanitize-labels value) + ;; Expected spec format: + ;; '(("foo" . "bar") "foo=bar") + (oci-sanitize-mixed-list "labels" value "=")) + +(define (oci-object-labels? value) + (list? (oci-sanitize-labels value))) + (define (oci-sanitize-shepherd-actions value) (map (lambda (el) @@ -358,6 +488,9 @@ (define (oci-sanitize-shepherd-actions value) but ~a was found") el)))) value)) +(define (oci-container-shepherd-actions? value) + (list? (oci-sanitize-shepherd-actions value))) + (define (oci-sanitize-extra-arguments value) (define (valid? member) (or (string? member) @@ -373,11 +506,19 @@ (define (oci-sanitize-extra-arguments value) but ~a was found") el)))) value)) +(define (oci-object-extra-arguments? value) + (list? (oci-sanitize-extra-arguments value))) + (define (oci-image-reference image) - (if (string? image) - image - (string-append (oci-image-repository image) - ":" (oci-image-tag image)))) + "Return a string OCI image reference representing IMAGE." + (define reference + (if (string? image) + image + (string-append (oci-image-repository image) + ":" (oci-image-tag image)))) + (if (> (length (string-split reference #\/)) 1) + reference + (string-append "localhost/" reference))) (define (oci-lowerable-image? image) (or (manifest? image) @@ -392,7 +533,19 @@ (define (string-or-oci-image? image) (define list-of-symbols? (list-of symbol?)) +(define (list-of-oci-records? name predicate value) + (map + (lambda (el) + (if (predicate el) + el + (raise + (formatted-message + (G_ "~a contains an illegal value: ~a") name el)))) + value)) + (define-maybe/no-serialization string) +(define-maybe/no-serialization package) +(define-maybe/no-serialization subid-range) (define-configuration/no-serialization oci-image (repository @@ -437,11 +590,15 @@ (define-configuration/no-serialization oci-image (define-configuration/no-serialization oci-container-configuration (user - (string "oci-container") - "The user under whose authority docker commands will be run.") + (maybe-string) + "The user name under whose authority OCI commands will be run. This field will +override the @code{user} field of @code{oci-configuration}.") (group - (string "docker") - "The group under whose authority docker commands will be run.") + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used. +This field will override the @code{group} field of @code{oci-configuration}.") (command (list-of-strings '()) "Overwrite the default command (@code{CMD}) of the image.") @@ -449,11 +606,11 @@ (define-configuration/no-serialization oci-container-configuration (maybe-string) "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.") (host-environment - (list '()) + (oci-container-host-environment '()) "Set environment variables in the host environment where @command{docker run} -is invoked. This is especially useful to pass secrets from the host to the -container without having them on the @command{docker run}'s command line: by -setting the @code{MYSQL_PASSWORD} on the host and by passing +or @command{podman run} are invoked. This is especially useful to pass secrets +from the host to the container without having them on the OCI runtime command line, +for example: by setting the @code{MYSQL_PASSWORD} on the host and by passing @code{--env MYSQL_PASSWORD} through the @code{extra-arguments} field, it is possible to securely set values in the container environment. This field's value can be a list of pairs or strings, even mixed: @@ -467,7 +624,7 @@ (define-configuration/no-serialization oci-container-configuration directly to @code{make-forkexec-constructor}." (sanitizer oci-sanitize-host-environment)) (environment - (list '()) + (oci-container-environment '()) "Set environment variables inside the container. This can be a list of pairs or strings, even mixed: @@ -477,15 +634,16 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#env,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#env,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#env-e-env,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-environment)) (image (string-or-oci-image) "The image used to build the container. It can be a string or an -@code{oci-image} record. Strings are resolved by the Docker -Engine, and follow the usual format +@code{oci-image} record. Strings are resolved by the OCI runtime, +and follow the usual format @code{myregistry.local:5000/testing/test-image:tag}.") (provision (maybe-string) @@ -508,15 +666,15 @@ (define-configuration/no-serialization oci-container-configuration "Whether to restart the service when it stops, for instance when the underlying process dies.") (shepherd-actions - (list '()) + (oci-container-shepherd-actions '()) "This is a list of @code{shepherd-action} records defining actions supported by the service." (sanitizer oci-sanitize-shepherd-actions)) (network (maybe-string) - "Set a Docker network for the spawned container.") + "Set an OCI network for the spawned container.") (ports - (list '()) + (oci-container-ports '()) "Set the port or port ranges to expose from the spawned container. This can be a list of pairs or strings, even mixed: @@ -526,12 +684,13 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#publish,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#publish,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#publish-p-ip-hostport-containerport-protocol,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-ports)) (volumes - (list '()) + (oci-container-volumes '()) "Set volume mappings for the spawned container. This can be a list of pairs or strings, even mixed: @@ -541,71 +700,352 @@ (define-configuration/no-serialization oci-container-configuration @end lisp Pair members can be strings, gexps or file-like objects. Strings are passed -directly to the Docker CLI. You can refer to the -@url{https://docs.docker.com/engine/reference/commandline/run/#volume,upstream} -documentation for semantics." +directly to the OCI runtime CLI. You can refer to the +@url{https://docs.docker.com/engine/reference/commandline/run/#volume,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#volume-v-source-volume-host-dir-container-dir-options,Podman} +upstream documentation for semantics." (sanitizer oci-sanitize-volumes)) (container-user (maybe-string) "Set the current user inside the spawned container. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#user,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#user,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#user-u-user-group,Podman} +upstream documentation for semantics.") (workdir (maybe-string) - "Set the current working for the spawned Shepherd service. + "Set the current working directory for the spawned Shepherd service. You can refer to the -@url{https://docs.docker.com/engine/reference/run/#workdir,upstream} -documentation for semantics.") +@url{https://docs.docker.com/engine/reference/run/#workdir,Docker} +or @url{https://docs.podman.io/en/stable/markdown/podman-run.1.html#workdir-w-dir,Podman} +upstream documentation for semantics.") (extra-arguments - (list '()) + (oci-object-extra-arguments '()) "A list of strings, gexps or file-like objects that will be directly passed -to the @command{docker run} invokation." +to the @command{docker run} or @command{podman run} invocation." (sanitizer oci-sanitize-extra-arguments))) -(define oci-container-configuration->options - (lambda (config) - (let ((entrypoint - (oci-container-configuration-entrypoint config)) - (network - (oci-container-configuration-network config)) - (user - (oci-container-configuration-container-user config)) - (workdir - (oci-container-configuration-workdir config))) - (apply append - (filter (compose not unspecified?) - `(,(if (maybe-value-set? entrypoint) - `("--entrypoint" ,entrypoint) - '()) - ,(append-map - (lambda (spec) - (list "--env" spec)) - (oci-container-configuration-environment config)) - ,(if (maybe-value-set? network) - `("--network" ,network) - '()) - ,(if (maybe-value-set? user) - `("--user" ,user) - '()) - ,(if (maybe-value-set? workdir) - `("--workdir" ,workdir) - '()) - ,(append-map - (lambda (spec) - (list "-p" spec)) - (oci-container-configuration-ports config)) - ,(append-map - (lambda (spec) - (list "-v" spec)) - (oci-container-configuration-volumes config)))))))) - -(define* (get-keyword-value args keyword #:key (default #f)) - (let ((kv (memq keyword args))) - (if (and kv (>= (length kv) 2)) - (cadr kv) - default))) +(define (list-of-oci-containers? value) + (list-of-oci-records? "containers" oci-container-configuration? value)) + +(define-configuration/no-serialization oci-volume-configuration + (name + (string) + "The name of the OCI volume to provision.") + (labels + (oci-object-labels '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (oci-object-extra-arguments '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker volume create} or @command{podman volume create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-volumes? value) + (list-of-oci-records? "volumes" oci-volume-configuration? value)) + +(define-configuration/no-serialization oci-network-configuration + (name + (string) + "The name of the OCI network to provision.") + (driver + (maybe-string) + "The driver to manage the network.") + (gateway + (maybe-string) + "IPv4 or IPv6 gateway for the subnet.") + (internal? + (boolean #f) + "Restrict external access to the network") + (ip-range + (maybe-string) + "Allocate container ip from a sub-range in CIDR format.") + (ipam-driver + (maybe-string) + "IP Address Management Driver.") + (ipv6? + (boolean #f) + "Enable IPv6 networking.") + (subnet + (maybe-string) + "Subnet in CIDR format that represents a network segment.") + (labels + (oci-object-labels '()) + "The list of labels that will be used to tag the current volume." + (sanitizer oci-sanitize-labels)) + (extra-arguments + (oci-object-extra-arguments '()) + "A list of strings, gexps or file-like objects that will be directly passed +to the @command{docker network create} or @command{podman network create} +invocation." + (sanitizer oci-sanitize-extra-arguments))) + +(define (list-of-oci-networks? value) + (list-of-oci-records? "networks" oci-network-configuration? value)) + +(define (package-or-string? value) + (or (package? value) (string? value))) + +(define-maybe/no-serialization package-or-string) + +(define-configuration/no-serialization oci-configuration + (runtime + (oci-runtime 'docker) + "The OCI runtime to use to run commands. It can be either @code{'docker} or +@code{'podman}.") + (runtime-cli + (maybe-package-or-string) + "The OCI runtime command line to be installed in the system profile and used +to provision OCI resources, it can be either a package or a string representing +an absolute file name to the runtime binary entrypoint. When unset it will default +to @code{docker-cli} package for the @code{'docker} runtime or to @code{podman} +package for the @code{'podman} runtime.") + (runtime-extra-arguments + (list '()) + "A list of strings, gexps or file-like objects that will be placed +after each @command{docker} or @command{podman} invokation.") + (user + (string "oci-container") + "The user name under whose authority OCI runtime commands will be run.") + (group + (maybe-string) + "The group name under whose authority OCI commands will be run. When +using the @code{'podman} OCI runtime, this field will be ignored and the +default group of the user configured in the @code{user} field will be used.") + (subuids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subuids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (subgids-range + (maybe-subid-range) + "An optional @code{subid-range} record allocating subgids for the user from +the @code{user} field. When unset, with the rootless Podman OCI runtime, it +defaults to @code{(subid-range (name \"oci-container\"))}.") + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to provision. The use of the @code{oci-extension} record should +be preferred for most cases.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to provision. The use of the @code{oci-extension} record should +be preferred for most cases.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to provision. The use of the @code{oci-extension} record should +be preferred for most cases.") + (verbose? + (boolean #f) + "When true, additional output will be printed, allowing to better follow the +flow of execution.") + (home-service? + (boolean for-home?) + "This is an internal field denoting whether this configuration is used in a +Guix Home context, as opposed to the default Guix System context.")) + +(define (oci-runtime-system-environment runtime user) + (if (eq? runtime 'podman) + (list + #~(string-append + "HOME=" (passwd:dir (getpwnam #$user)))) + #~())) + +(define (oci-runtime-cli runtime runtime-cli profile-directory) + "Return a gexp that, when lowered, evaluates to the of the OCI +runtime command requested by the user." + (if (string? runtime-cli) + ;; It is a user defined absolute file name. + runtime-cli + #~(string-append + #$(if (maybe-value-set? runtime-cli) + runtime-cli + profile-directory) + #$(if (eq? 'podman runtime) + "/bin/podman" + "/bin/docker")))) + +(define* (oci-runtime-system-cli config #:key (profile-directory "/run/current-system/profile")) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli profile-directory))) + +(define (oci-runtime-home-cli config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-runtime-cli runtime runtime-cli + (string-append (getenv "HOME") + "/.guix-home/profile")))) + +(define-configuration/no-serialization oci-extension + (containers + (list-of-oci-containers '()) + "The list of @code{oci-container-configuration} records representing the +containers to add.") + (networks + (list-of-oci-networks '()) + "The list of @code{oci-network-configuration} records representing the +networks to add.") + (volumes + (list-of-oci-volumes '()) + "The list of @code{oci-volume-configuration} records representing the +volumes to add.")) + +(define (oci-image->container-name image) + "Infer the name of an OCI backed Shepherd service from its OCI image." + (basename + (if (string? image) + (first (string-split image #\:)) + (oci-image-repository image)))) + +(define (oci-command-line-shepherd-action object-name invocation entrypoint) + "Return a Shepherd action printing a given INVOCATION of an OCI command for the +given OBJECT-NAME." + (shepherd-action + (name 'command-line) + (documentation + (format #f "Prints ~a's OCI runtime command line invocation." + object-name)) + (procedure + #~(lambda _ + (format #t "Entrypoint:~%~a~%" #$entrypoint) + (format #t "Invocation:~%~a~%" #$invocation))))) + +(define (oci-container-shepherd-name runtime config) + "Return the name of an OCI backed Shepherd service based on CONFIG. +The name configured in the configuration record is returned when +CONFIG's name field has a value, otherwise a name is inferred from CONFIG's +image field." + (define name (oci-container-configuration-provision config)) + (define image (oci-container-configuration-image config)) + + (if (maybe-value-set? name) + name + (string-append (symbol->string runtime) "-" + (oci-image->container-name image)))) + +(define (oci-networks-shepherd-name runtime) + "Return the name of the OCI networks provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-networks")) + +(define (oci-volumes-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Shepherd service based on +RUNTIME." + (string-append (symbol->string runtime) "-volumes")) + +(define (oci-networks-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-networks-shepherd-name runtime))) + +(define (oci-volumes-home-shepherd-name runtime) + "Return the name of the OCI volumes provisioning Home Shepherd service based on +RUNTIME." + (string-append "home-" (oci-volumes-shepherd-name runtime))) + +(define (oci-container-configuration->options config) + "Map CONFIG, an oci-container-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime run command." + (let ((entrypoint + (oci-container-configuration-entrypoint config)) + (network + (oci-container-configuration-network config)) + (user + (oci-container-configuration-container-user config)) + (workdir + (oci-container-configuration-workdir config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? entrypoint) + `("--entrypoint" ,entrypoint) + '()) + ,(append-map + (lambda (spec) + (list "--env" spec)) + (oci-container-configuration-environment config)) + ,(if (maybe-value-set? network) + `("--network" ,network) + '()) + ,(if (maybe-value-set? user) + `("--user" ,user) + '()) + ,(if (maybe-value-set? workdir) + `("--workdir" ,workdir) + '()) + ,(append-map + (lambda (spec) + (list "-p" spec)) + (oci-container-configuration-ports config)) + ,(append-map + (lambda (spec) + (list "-v" spec)) + (oci-container-configuration-volumes config))))))) + +(define (oci-network-configuration->options config) + "Map CONFIG, an oci-network-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime network create command." + (let ((driver (oci-network-configuration-driver config)) + (gateway + (oci-network-configuration-gateway config)) + (internal? + (oci-network-configuration-internal? config)) + (ip-range + (oci-network-configuration-ip-range config)) + (ipam-driver + (oci-network-configuration-ipam-driver config)) + (ipv6? + (oci-network-configuration-ipv6? config)) + (subnet + (oci-network-configuration-subnet config))) + (apply append + (filter (compose not unspecified?) + `(,(if (maybe-value-set? driver) + `("--driver" ,driver) + '()) + ,(if (maybe-value-set? gateway) + `("--gateway" ,gateway) + '()) + ,(if internal? + `("--internal") + '()) + ,(if (maybe-value-set? ip-range) + `("--ip-range" ,ip-range) + '()) + ,(if (maybe-value-set? ipam-driver) + `("--ipam-driver" ,ipam-driver) + '()) + ,(if ipv6? + `("--ipv6") + '()) + ,(if (maybe-value-set? subnet) + `("--subnet" ,subnet) + '()) + ,(append-map + (lambda (spec) + (list "--label" spec)) + (oci-network-configuration-labels config))))))) + +(define (oci-volume-configuration->options config) + "Map CONFIG, an oci-volume-configuration record, to a gexp that, upon +lowering, will be evaluated to a list of strings containing command line options +for the OCI runtime volume create command." + (append-map + (lambda (spec) + (list "--label" spec)) + (oci-volume-configuration-labels config))) (define (lower-operating-system os target system) + "Lower OS, an operating-system record, into a tarball containing an OCI image." (mlet* %store-monad ((tarball (lower-object @@ -614,24 +1054,11 @@ (define (lower-operating-system os target system) #:target target))) (return tarball))) -(define (lower-manifest name image target system) - (define value (oci-image-value image)) - (define options (oci-image-pack-options image)) - (define image-reference - (oci-image-reference image)) - (define image-tag - (let* ((extra-options - (get-keyword-value options #:extra-options)) - (image-tag-option - (and extra-options - (get-keyword-value extra-options #:image-tag)))) - (if image-tag-option - '() - `(#:extra-options (#:image-tag ,image-reference))))) - +(define (lower-manifest name value options image-reference + target system grafts?) + "Lower VALUE, a manifest record, into a tarball containing an OCI image." (mlet* %store-monad - ((_ (set-grafting - (oci-image-grafts? image))) + ((_ (set-grafting grafts?)) (guile (set-guile-for-build (default-guile))) (profile (profile-derivation value @@ -642,14 +1069,11 @@ (define (lower-manifest name image target system) (tarball (apply pack:docker-image `(,name ,profile ,@options - ,@image-tag #:localstatedir? #t)))) (return tarball))) -(define (lower-oci-image name image) - (define value (oci-image-value image)) - (define image-target (oci-image-target image)) - (define image-system (oci-image-system image)) +(define (lower-oci-image-state name value options reference + image-target image-system grafts?) (define target (if (maybe-value-set? image-target) image-target @@ -662,7 +1086,8 @@ (define (lower-oci-image name image) (run-with-store store (match value ((? manifest? value) - (lower-manifest name image target system)) + (lower-manifest name value options reference + target system grafts?)) ((? operating-system? value) (lower-operating-system value target system)) ((or (? gexp? value) @@ -677,113 +1102,663 @@ (define (lower-oci-image name image) #:target target #:system system))) -(define (%oci-image-loader name image tag) - (let ((docker (file-append docker-cli "/bin/docker")) - (tarball (lower-oci-image name image))) - (with-imported-modules '((guix build utils)) - (program-file (format #f "~a-image-loader" name) +(define (lower-oci-image name image) + "Lower IMAGE, a oci-image record, into a tarball containing an OCI image." + (lower-oci-image-state + name + (oci-image-value image) + (oci-image-pack-options image) + (oci-image-reference image) + (oci-image-target image) + (oci-image-system image) + (oci-image-grafts? image))) + +(define-record-type* <oci-runtime-state> + oci-runtime-state + make-oci-runtime-state + oci-runtime-state? + this-oci-runtime-state + + (runtime oci-runtime-state-runtime + (default 'docker)) + (runtime-cli oci-runtime-state-runtime-cli) + (user oci-runtime-state-user) + (group oci-runtime-state-group) + (runtime-environment oci-runtime-state-runtime-environment + (default #~())) + (runtime-requirement oci-runtime-state-runtime-requirement + (default '())) + (runtime-extra-arguments oci-runtime-state-runtime-extra-arguments + (default '()))) + +(define-record-type* <oci-state> + oci-state + make-oci-state + oci-state? + this-oci-state + + (networks oci-state-networks) + (volumes oci-state-volumes) + (containers oci-state-containers) + (networks-name oci-state-networks-name + (default #f)) + (volumes-name oci-state-volumes-name + (default #f)) + (networks-requirement oci-state-networks-requirement + (default '())) + (volumes-requirement oci-state-volumes-requirement + (default '())) + (containers-requirement oci-state-containers-requirement + (default '()))) + +(define-record-type* <oci-container-invocation> + oci-container-invocation + make-oci-container-invocation + oci-container-invocation? + this-oci-container-invocation + + (runtime oci-container-invocation-runtime + (default 'docker)) + (runtime-cli oci-container-invocation-runtime-cli) + (name oci-container-invocation-name) + (command oci-container-invocation-command + (default '())) + (image-reference oci-container-invocation-image-reference) + (options oci-container-invocation-options + (default '())) + (run-extra-arguments oci-container-invocation-run-extra-arguments + (default '())) + (runtime-extra-arguments oci-container-invocation-runtime-extra-arguments + (default '()))) + +(define (oci-container-configuration->oci-container-invocation runtime-state + config) + (oci-container-invocation + (runtime (oci-runtime-state-runtime runtime-state)) + (runtime-cli (oci-runtime-state-runtime-cli runtime-state)) + (name + (oci-container-shepherd-name runtime config)) + (command + (oci-container-configuration-command config)) + (image-reference + (oci-image-reference (oci-container-configuration-image config))) + (options + (oci-container-configuration->options config)) + (run-extra-arguments + (oci-container-configuration-extra-arguments config)) + (runtime-extra-arguments + (oci-runtime-state-runtime-extra-arguments runtime-state)))) + +(define* (oci-image-loader runtime-state name image tag #:key verbose?) + "Return a file-like object that, once lowered, will evaluate to a program able +to load IMAGE through RUNTIME-CLI and to tag it with TAG afterwards." + (let ((tarball (lower-oci-image name image))) + (with-imported-modules (source-module-closure '((gnu build oci-containers))) + (program-file + (format #f "~a-image-loader" name) #~(begin - (use-modules (guix build utils) - (ice-9 popen) - (ice-9 rdelim)) - - (format #t "Loading image for ~a from ~a...~%" #$name #$tarball) - (define line - (read-line - (open-input-pipe - (string-append #$docker " load -i " #$tarball)))) - - (unless (or (eof-object? line) - (string-null? line)) - (format #t "~a~%" line) - (let ((repository&tag - (string-drop line - (string-length - "Loaded image: ")))) - - (invoke #$docker "tag" repository&tag #$tag) - (format #t "Tagged ~a with ~a...~%" #$tarball #$tag)))))))) - -(define (oci-container-shepherd-service config) - (define (guess-name name image) - (if (maybe-value-set? name) - name - (string-append "docker-" - (basename - (if (string? image) - (first (string-split image #\:)) - (oci-image-repository image)))))) - - (let* ((docker (file-append docker-cli "/bin/docker")) - (actions (oci-container-configuration-shepherd-actions config)) - (auto-start? - (oci-container-configuration-auto-start? config)) - (user (oci-container-configuration-user config)) - (group (oci-container-configuration-group config)) - (host-environment - (oci-container-configuration-host-environment config)) - (command (oci-container-configuration-command config)) - (log-file (oci-container-configuration-log-file config)) - (provision (oci-container-configuration-provision config)) - (requirement (oci-container-configuration-requirement config)) - (respawn? - (oci-container-configuration-respawn? config)) - (image (oci-container-configuration-image config)) - (image-reference (oci-image-reference image)) - (options (oci-container-configuration->options config)) - (name (guess-name provision image)) - (extra-arguments - (oci-container-configuration-extra-arguments config))) - - (shepherd-service (provision `(,(string->symbol name))) - (requirement `(dockerd user-processes ,@requirement)) + (use-modules (gnu build oci-containers)) + (oci-image-load '#$(oci-runtime-state-runtime runtime-state) + #$(oci-runtime-state-runtime-cli runtime-state) + #$tarball #$name #$tag + #:verbose? #$verbose?)))))) + +(define (oci-container-run-invocation container-invocation) + "Return a list representing the OCI runtime +invocation for running containers." + ;; run [OPTIONS] IMAGE [COMMAND] [ARG...] + `(,(oci-container-invocation-runtime-cli container-invocation) + ,@(oci-container-invocation-runtime-extra-arguments container-invocation) + "run" "--rm" + ,@(if (eq? (oci-container-invocation-runtime container-invocation) + 'podman) + ;; This is because podman takes some time to + ;; release container names. --replace seems + ;; to be required to be able to restart services. + '("--replace") + '()) + "--name" ,(oci-container-invocation-name container-invocation) + ,@(oci-container-invocation-options container-invocation) + ,@(oci-container-invocation-run-extra-arguments container-invocation) + ,(oci-container-invocation-image-reference container-invocation) + ,@(oci-container-invocation-command container-invocation))) + +(define* (oci-container-entrypoint name invocation + #:key verbose? + (pre-script #~())) + "Return a file-like object that, once lowered, will evaluate to the entrypoint +for the Shepherd service that will run INVOCATION." + (program-file + (string-append "oci-entrypoint-" name) + (with-imported-modules (source-module-closure + '((gnu build oci-containers))) + #~(begin + (use-modules (gnu build oci-containers) + (srfi srfi-1)) + (oci-container-execlp + (list #$@invocation) + #:verbose? #$verbose? + #:pre-script + (lambda _ + (when (and #$verbose? + (zero? (length '(#$@pre-script)))) + (format #t "No pre script to run...")) + #$@pre-script)))))) + +(define* (oci-container-shepherd-service state runtime-state config + #:key verbose? + networks? + volumes?) + "Return a Shepherd service object that will run the OCI container represented +by CONFIG through RUNTIME-CLI." + (match-record config <oci-container-configuration> + (shepherd-actions auto-start? user group host-environment + log-file requirement respawn? image) + (define runtime (oci-runtime-state-runtime runtime-state)) + (define runtime-cli (oci-runtime-state-runtime-cli runtime-state)) + (define image-reference (oci-image-reference image)) + (define shepherd-name (oci-container-shepherd-name runtime config)) + (define oci-container-user + (if (maybe-value-set? user) + user + (oci-runtime-state-user runtime-state))) + (define oci-container-group + (if (maybe-value-set? group) + group + (oci-runtime-state-group runtime-state))) + (define networks-service + (if networks? + (list + (string->symbol + (oci-state-networks-name state))) + '())) + (define volumes-service + (if volumes? + (list + (string->symbol + (oci-state-volumes-name state))) + '())) + (define oci-container-requirement + (append requirement + (oci-state-containers-requirement state) + (oci-runtime-state-runtime-requirement runtime-state) + networks-service + volumes-service)) + (define environment-variables + #~(append + (list #$@host-environment) + (list #$@(oci-runtime-state-runtime-environment runtime-state)))) + (define invocation + (oci-container-run-invocation + (oci-container-configuration->oci-container-invocation + runtime-state config))) + (define* (container-action command) + #~(lambda _ + (fork+exec-command + (list #$@command) + #$@(if oci-container-user + (list #:user oci-container-user) + '()) + #$@(if oci-container-group + (list #:group oci-container-group) + '()) + #$@(if (maybe-value-set? log-file) + (list #:log-file log-file) + '()) + #$@(if (and oci-container-user (eq? runtime 'podman)) + (list #:directory + #~(passwd:dir + (getpwnam #$oci-container-user))) + '()) + #:environment-variables + #$environment-variables))) + (define start-entrypoint + (oci-container-entrypoint + shepherd-name invocation + #:verbose? verbose? + #:pre-script + (if (oci-image? image) + #~((system* + #$(oci-image-loader + runtime-state shepherd-name image + image-reference + #:verbose? verbose?))) + #~()))) + + (shepherd-service (provision `(,(string->symbol shepherd-name))) + (requirement oci-container-requirement) (respawn? respawn?) (auto-start? auto-start?) (documentation (string-append - "Docker backed Shepherd service for " - (if (oci-image? image) name image) ".")) + (oci-runtime-name runtime) + " backed Shepherd service for " + (if (oci-image? image) shepherd-name image) ".")) (start - #~(lambda () - #$@(if (oci-image? image) - #~((invoke #$(%oci-image-loader - name image image-reference))) - #~()) - (fork+exec-command - ;; docker run [OPTIONS] IMAGE [COMMAND] [ARG...] - (list #$docker "run" "--rm" "--name" #$name - #$@options #$@extra-arguments - #$image-reference #$@command) - #:user #$user - #:group #$group - #$@(if (maybe-value-set? log-file) - (list #:log-file log-file) - '()) - #:environment-variables - (list #$@host-environment)))) + (container-action + (list start-entrypoint))) (stop - #~(lambda _ - (invoke #$docker "rm" "-f" #$name))) + (container-action + (list + (oci-container-entrypoint + shepherd-name (list runtime-cli "rm" "-f" shepherd-name) + #:verbose? verbose?)))) (actions - (if (oci-image? image) - '() - (append + (append + (list + (oci-command-line-shepherd-action + shepherd-name #~(string-join (list #$@invocation) " ") + start-entrypoint)) + (if (oci-image? image) + '() (list (shepherd-action (name 'pull) (documentation (format #f "Pull ~a's image (~a)." - name image)) + shepherd-name image)) (procedure - #~(lambda _ - (invoke #$docker "pull" #$image))))) - actions)))))) - -(define %oci-container-accounts + (container-action + (list + (oci-container-entrypoint + shepherd-name (list runtime-cli "pull" image) + #:verbose? verbose?))))))) + shepherd-actions))))) + +(define (oci-object-create-invocation object runtime-cli name options + runtime-extra-arguments + create-extra-arguments) + "Return a gexp that, upon lowering, will evaluate to the OCI runtime +invocation for creating networks and volumes." + ;; network|volume create [options] [NAME] + #~(list #$runtime-cli #$@runtime-extra-arguments #$object "create" + #$@options #$@create-extra-arguments #$name)) + +(define (format-oci-invocations invocations) + "Return a gexp that, upon lowering, will evaluate to a formatted message +containing the INVOCATIONS that the OCI runtime will execute to provision +networks or volumes." + #~(string-join (map (lambda (i) (string-join i " ")) + (list #$@invocations)) + "\n")) + +(define* (oci-object-create-script object runtime runtime-cli invocations + #:key verbose?) + "Return a file-like object that, once lowered, will evaluate to a program able +to create OCI networks and volumes through RUNTIME-CLI." + (define runtime-string (symbol->string runtime)) + (define runtime-name (oci-runtime-name runtime)) + (with-imported-modules (source-module-closure + '((gnu build oci-containers))) + + (program-file + (string-append runtime-string "-" object "s-create.scm") + #~(begin + (use-modules (gnu build oci-containers)) + (oci-object-create '#$runtime #$runtime-cli #$runtime-name + #$object (list #$@invocations) + #:verbose? #$verbose?))))) + +(define* (oci-object-shepherd-service object runtime-state name + oci-state-requirement invocations + #:key verbose?) + "Return a Shepherd service object that will provision the OBJECTs represented +by INVOCATIONS through RUNTIME-STATE." + (match-record runtime-state <oci-runtime-state> + (runtime runtime-cli runtime-requirement user group + runtime-environment) + (define entrypoint + (oci-object-create-script + object runtime runtime-cli invocations #:verbose? verbose?)) + (define requirement + (append runtime-requirement oci-state-requirement)) + + (shepherd-service (provision (list (string->symbol name))) + (requirement requirement) + (one-shot? #t) + (documentation + (string-append + (oci-runtime-name runtime) " " object + " provisioning service")) + (start + #~(lambda _ + (fork+exec-command + (list #$entrypoint) + #$@(if user (list #:user user) '()) + #$@(if group (list #:group group) '()) + #:environment-variables + (list #$@runtime-environment)))) + (actions + (list + (oci-command-line-shepherd-action + name (format-oci-invocations invocations) + entrypoint)))))) + +(define* (oci-networks-shepherd-service state runtime-state + #:key verbose?) + "Return a Shepherd service object that will create the networks represented +in STATE." + (define runtime-cli + (oci-runtime-state-runtime-cli runtime-state)) + (define invocations + (map + (lambda (network) + (oci-object-create-invocation + "network" runtime-cli + (oci-network-configuration-name network) + (oci-network-configuration->options network) + (oci-runtime-state-runtime-extra-arguments runtime-state) + (oci-network-configuration-extra-arguments network))) + (oci-state-networks state))) + + (oci-object-shepherd-service + "network" runtime-state (oci-state-networks-name state) + (oci-state-networks-requirement state) + invocations #:verbose? verbose?)) + +(define* (oci-volumes-shepherd-service state runtime-state + #:key verbose?) + "Return a Shepherd service object that will create the volumes represented +in STATE." + (define runtime-cli + (oci-runtime-state-runtime-cli runtime-state)) + (define invocations + (map + (lambda (volume) + (oci-object-create-invocation + "volume" runtime-cli + (oci-volume-configuration-name volume) + (oci-volume-configuration->options volume) + (oci-runtime-state-runtime-extra-arguments runtime-state) + (oci-volume-configuration-extra-arguments volume))) + (oci-state-volumes state))) + + (oci-object-shepherd-service + "volume" runtime-state (oci-state-volumes-name state) + (oci-state-volumes-requirement state) + invocations #:verbose? verbose?)) + +(define (oci-service-accounts config) + (define user (oci-configuration-user config)) + (define maybe-group (oci-configuration-group config)) + (define runtime (oci-configuration-runtime config)) (list (user-account - (name "oci-container") + (name user) (comment "OCI services account") - (group "docker") - (system? #t) - (home-directory "/var/empty") + (group "users") + (supplementary-groups + (list (oci-runtime-group runtime maybe-group))) + (system? (eq? 'docker runtime)) + (home-directory (if (eq? 'podman runtime) + (string-append "/home/" user) + "/var/empty")) + (create-home-directory? (eq? 'podman runtime)) (shell (file-append shadow "/sbin/nologin"))))) + +(define* (oci-state->shepherd-services state runtime-state #:key verbose?) + "Returns a list of Shepherd services based on the input OCI state." + (define networks? + (> (length (oci-state-networks state)) 0)) + (define volumes? + (> (length (oci-state-volumes state)) 0)) + (append + (map + (lambda (c) + (oci-container-shepherd-service + state runtime-state c + #:verbose? verbose? + #:volumes? volumes? + #:networks? networks?)) + (oci-state-containers state)) + (if networks? + (list + (oci-networks-shepherd-service + state runtime-state + #:verbose? verbose?)) + '()) + (if volumes? + (list + (oci-volumes-shepherd-service + state runtime-state + #:verbose? verbose?)) + '()))) + +(define* (oci-configuration->oci-runtime-state config #:key verbose?) + (define runtime + (oci-configuration-runtime config)) + (define home-service? + (oci-configuration-home-service? config)) + (when verbose? + (format #t "Home service: ~a~%" home-service?)) + (define runtime-cli + (if home-service? + (oci-runtime-home-cli config) + (oci-runtime-system-cli config))) + (define user + (if home-service? + #f + (oci-configuration-user config))) + (define group + (if home-service? + #f + (if (eq? runtime 'podman) + #~(group:name + (getgrgid + (passwd:gid + (getpwnam #$user)))) + (oci-runtime-group config (oci-configuration-group config))))) + (define runtime-requirement + (if home-service? + '() + (oci-runtime-system-requirement runtime))) + (define runtime-environment + (if home-service? + #~() + (oci-runtime-system-environment runtime user))) + (oci-runtime-state + (runtime runtime) + (runtime-cli runtime-cli) + (user user) + (group group) + (runtime-extra-arguments + (oci-configuration-runtime-extra-arguments config)) + (runtime-environment runtime-environment) + (runtime-requirement runtime-requirement))) + +(define (oci-configuration->oci-state config) + (define runtime + (oci-configuration-runtime config)) + (define home-service? + (oci-configuration-home-service? config)) + (define networks-name + (if home-service? + (oci-networks-home-shepherd-name runtime) + (oci-networks-shepherd-name runtime))) + (define volumes-name + (if home-service? + (oci-volumes-home-shepherd-name runtime) + (oci-volumes-shepherd-name runtime))) + (define networks-requirement + (if home-service? + '() + '(networking))) + (oci-state + (containers (oci-configuration-containers config)) + (networks (oci-configuration-networks config)) + (volumes (oci-configuration-volumes config)) + (networks-name networks-name) + (volumes-name volumes-name) + (networks-requirement networks-requirement))) + +(define (oci-configuration->shepherd-services config) + (let* ((verbose? (oci-configuration-verbose? config)) + (state (oci-configuration->oci-state config)) + (runtime-state + (oci-configuration->oci-runtime-state config #:verbose? verbose?))) + (oci-state->shepherd-services state runtime-state #:verbose? verbose?))) + +(define (oci-service-subids config) + "Return a subids-extension record representing subuids and subgids required by +the rootless Podman backend." + (define (find-duplicates subids) + (let loop ((names '()) + (subids subids)) + (if (null? names) + names + (loop + (let ((name (subid-range-name (car subids)))) + (if (member name names) + (raise + (formatted-message + (G_ "Duplicated subid-range: ~a. subid-ranges names should be +unique, please remove the duplicate.") name)) + (cons name names))) + (cdr subids))))) + + (define runtime + (oci-configuration-runtime config)) + (define user + (oci-configuration-user config)) + + (define subgids (oci-configuration-subgids-range config)) + (find-duplicates subgids) + + (define subuids (oci-configuration-subuids-range config)) + (find-duplicates subgids) + + (define container-users + (filter (lambda (range) + (and (maybe-value-set? + (subid-range-name range)) + (not (string=? (subid-range-name range) user)))) + (map (lambda (container) + (subid-range + (name + (oci-container-configuration-user container)))) + (oci-configuration-containers config)))) + (define subgid-ranges + (cons + (if (maybe-value-set? subgids) + subgids + (subid-range (name user))) + container-users)) + (define subuid-ranges + (cons + (if (maybe-value-set? subuids) + subuids + (subid-range (name user))) + container-users)) + + (if (eq? 'podman runtime) + (subids-extension + (subgids + subgid-ranges) + (subuids + subuid-ranges)) + (subids-extension))) + +(define (oci-objects-merge-lst a b object get-name) + (define (contains? value lst) + (member value (map get-name lst))) + (let loop ((merged '()) + (lst (append a b))) + (if (null? lst) + merged + (loop + (let ((element (car lst))) + (when (contains? element merged) + (raise + (formatted-message + (G_ "Duplicated ~a: ~a. Names of ~a should be unique, please +remove the duplicate.") object (get-name element) object))) + (cons element merged)) + (cdr lst))))) + +(define (oci-extension-merge a b) + (oci-extension + (containers (oci-objects-merge-lst + (oci-extension-containers a) + (oci-extension-containers b) + "container" + (lambda (config) + (define maybe-name + (oci-container-configuration-provision config)) + (if (maybe-value-set? maybe-name) + maybe-name + (oci-image->container-name + (oci-container-configuration-image config)))))) + (networks (oci-objects-merge-lst + (oci-extension-networks a) + (oci-extension-networks b) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-extension-volumes a) + (oci-extension-volumes b) + "volume" + oci-volume-configuration-name)))) + +(define (oci-service-profile runtime runtime-cli) + `(,bash-minimal + ,@(if (string? runtime-cli) + '() + (list + (cond + ((maybe-value-set? runtime-cli) + runtime-cli) + ((eq? 'podman runtime) + podman) + (else + docker-cli)))))) + +(define (oci-configuration-extend config extension) + (oci-configuration + (inherit config) + (containers + (oci-objects-merge-lst + (oci-configuration-containers config) + (oci-extension-containers extension) + "container" + (lambda (oci-config) + (define runtime + (oci-configuration-runtime config)) + (oci-container-shepherd-name runtime oci-config)))) + (networks (oci-objects-merge-lst + (oci-configuration-networks config) + (oci-extension-networks extension) + "network" + oci-network-configuration-name)) + (volumes (oci-objects-merge-lst + (oci-configuration-volumes config) + (oci-extension-volumes extension) + "volume" + oci-volume-configuration-name)))) + +(define oci-service-type + (service-type + (name 'oci) + (extensions + (list + (service-extension profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension subids-service-type + oci-service-subids) + (service-extension account-service-type + oci-service-accounts) + (service-extension shepherd-root-service-type + oci-configuration->shepherd-services))) + ;; Concatenate OCI object lists. + (compose (lambda (args) + (fold oci-extension-merge + (oci-extension) + args))) + (extend oci-configuration-extend) + (default-value (oci-configuration)) + (description + "This service implements the provisioning of OCI objects such +as containers, networks and volumes."))) diff --git a/gnu/services/docker.scm b/gnu/services/docker.scm index 828ceea313a..6abfbc49a0b 100644 --- a/gnu/services/docker.scm +++ b/gnu/services/docker.scm @@ -31,7 +31,10 @@ (define-module (gnu services docker) #:use-module (gnu system shadow) #:use-module (gnu packages docker) #:use-module (gnu packages linux) ;singularity + #:use-module (guix deprecation) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (srfi srfi-1) #:use-module (ice-9 format) @@ -67,16 +70,18 @@ (define-module (gnu services docker) oci-container-configuration-volumes oci-container-configuration-container-user oci-container-configuration-workdir - oci-container-configuration-extra-arguments - oci-container-shepherd-service - %oci-container-accounts) + oci-container-configuration-extra-arguments) #:export (containerd-configuration containerd-service-type docker-configuration docker-service-type singularity-service-type - oci-container-service-type)) + ;; For backwards compatibility, until the + ;; oci-container-service-type is fully deprecated. + oci-container-shepherd-service + oci-container-service-type + %oci-container-accounts)) (define-maybe file-like) @@ -297,17 +302,26 @@ (define singularity-service-type ;;; OCI container. ;;; -(define (configs->shepherd-services configs) - (map oci-container-shepherd-service configs)) +;; For backwards compatibility, until the +;; oci-container-service-type is fully deprecated. +(define-deprecated (oci-container-shepherd-service config) + oci-service-type + ((@ (gnu services containers) oci-container-shepherd-service) + 'docker config)) +(define %oci-container-accounts + (filter user-account? (oci-service-accounts (oci-configuration)))) (define oci-container-service-type (service-type (name 'oci-container) - (extensions (list (service-extension profile-service-type - (lambda _ (list docker-cli))) - (service-extension account-service-type - (const %oci-container-accounts)) - (service-extension shepherd-root-service-type - configs->shepherd-services))) + (extensions + (list (service-extension oci-service-type + (lambda (containers) + (warning + (G_ + "'oci-container-service-type' is\ + deprecated, use 'oci-service-type' instead~%")) + (oci-extension + (containers containers)))))) (default-value '()) (extend append) (compose concatenate) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 0ecc8ddb126..051a4c740bf 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -27,6 +27,9 @@ (define-module (gnu tests containers) #:use-module (gnu services) #:use-module (gnu services containers) #:use-module (gnu services desktop) + #:use-module ((gnu services docker) + #:select (containerd-service-type + docker-service-type)) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu system) @@ -39,7 +42,9 @@ (define-module (gnu tests containers) #:use-module (guix profiles) #:use-module ((guix scripts pack) #:prefix pack:) #:use-module (guix store) - #:export (%test-rootless-podman)) + #:export (%test-rootless-podman + %test-oci-service-rootless-podman + %test-oci-service-docker)) (define %rootless-podman-os @@ -133,7 +138,7 @@ (define (run-rootless-podman-test oci-tarball) (status (close-pipe port))) output))) (let* ((bash - ,(string-append #$bash "/bin/bash")) + (string-append #$bash "/bin/bash")) (response1 (slurp bash "-c" (string-append "ls -la /sys/fs/cgroup | " @@ -345,3 +350,555 @@ (define %test-rootless-podman (name "rootless-podman") (description "Test rootless Podman service.") (value (build-tarball&run-rootless-podman-test)))) + + +(define %oci-network + (oci-network-configuration (name "my-network"))) + +(define %oci-volume + (oci-volume-configuration (name "my-volume"))) + +(define %oci-wait-for-file + #~(define (wait-for-file file) + ;; Wait until FILE shows up. + (let loop ((i 6)) + (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 %oci-read-lines + #~(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 %oci-slurp + #~(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)))) + +(define (%oci-rootless-podman-run commands) + #~((use-modules (srfi srfi-1) + (ice-9 format) + (ice-9 popen) + (ice-9 match) + (ice-9 rdelim) + (gnu build oci-containers)) + + #$%oci-wait-for-file + #$%oci-read-lines + #$%oci-slurp + + (define responses + (map + (lambda (index) + (format #f "/tmp/response_~a" index)) + (iota (length '#$commands)))) + + (match (primitive-fork) + (0 + (begin + (setgid (passwd:gid (getpwnam "oci-container"))) + (setuid (passwd:uid (getpw "oci-container"))) + + (let* ((outputs + (list #$@commands)) + (outputs-responses + (zip outputs responses))) + (for-each + (match-lambda + ((output response) + (call-with-output-file response + (lambda (port) + (display (string-join output "\n") port))))) + outputs-responses)))) + (pid + (cdr (waitpid pid)))) + + (for-each wait-for-file responses) + (map + (lambda (response) + (sort (slurp "cat" response) string<=?)) + responses))) + +(define %oci-rootless-podman-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service iptables-service-type) + (service rootless-podman-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (runtime 'podman) + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list %oci-network)) + (volumes + (list %oci-volume)) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300)) +(display c) +(newline) +(sleep 1) +(when (positive? c) + (l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-rootless-podman-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-rootless-podman-os + (list)) + #:imported-modules '((gnu build oci-containers) + (gnu build utils) + (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 oci-containers) + (gnu build utils) + (gnu build marionette)) + #~(begin + (use-modules (srfi srfi-1) (srfi srfi-11) (srfi srfi-64) + (gnu build utils) + (gnu build marionette)) + + (define marionette + ;; Relax timeout to accommodate older systems and + ;; allow for pulling the image. + (make-marionette (list #$vm) #:timeout 60)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "rootless-podman-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'user-processes)) + marionette) + + (test-assert "podman-volumes running" + (begin + (define (run-test) + (first + (marionette-eval + `(begin + #$@(%oci-rootless-podman-run + #~((oci-object-service-available? + "/run/current-system/profile/bin/podman" + "volume" + '("my-volume") + #:verbose? #t)))) + marionette))) + ;; Allow services to come up on slower machines. + (with-retries 80 1 (equal? '("my-volume") (run-test))))) + + (test-assert "podman-networks running" + (begin + (define (run-test) + (first + (marionette-eval + `(begin + #$@(%oci-rootless-podman-run + #~((oci-object-service-available? + "/run/current-system/profile/bin/podman" + "network" + '("my-network") + #:verbose? #t)))) + marionette))) + ;; Allow services to come up on slower machines. + (with-retries 80 1 (equal? '("my-network" "podman") (run-test))))) + + (test-assert "image loaded" + (begin + (define (run-test) + (first + (marionette-eval + `(begin + #$@(%oci-rootless-podman-run + #~((oci-object-service-available? + "/run/current-system/profile/bin/podman" + "image" + '("localhost/guile:latest") + #:format-string "{{.Repository}}:{{.Tag}}" + #:verbose? #t)))) + marionette))) + ;; Allow services to come up on slower machines. + (with-retries 80 1 + (equal? + '("localhost/guile:latest") + (run-test))))) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (first + (marionette-eval + `(begin + #$@(%oci-rootless-podman-run + #~((slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" + "'(display (getenv \"VARIABLE\"))'")))) + marionette))) + ;; Allow services to come up on slower machines. + (with-retries 80 1 (equal? '("value") (run-test))))) + + (test-equal "mounting host files" + '("hello") + (first + (marionette-eval + `(begin + #$@(%oci-rootless-podman-run + #~((slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin +(use-modules (ice-9 popen) (ice-9 rdelim)) +(display (call-with-input-file \"/shared.txt\" read-line)))'")))) + marionette))) + + (test-equal "read and write to provisioned volumes" + '("world") + (second + (marionette-eval + `(begin + #$@(%oci-rootless-podman-run + #~((slurp + "/run/current-system/profile/bin/podman" + "exec" "first" + "/bin/guile" "-c" "'(begin +(use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" + (lambda (p) (display \"world\" p))))'") + (slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin +(use-modules (ice-9 popen) (ice-9 rdelim)) +(display + (call-with-input-file \"/my-volume/out.txt\" read-line)))'")))) + marionette))) + + (test-equal + "can read and write to ports over provisioned network" + '("out of office") + (first + (marionette-eval + `(begin + #$@(%oci-rootless-podman-run + #~((slurp + "/run/current-system/profile/bin/podman" + "exec" "second" + "/bin/guile" "-c" "'(begin +(use-modules (web client)) +(define-values (response out) (http-get \"http://first:8080\")) +(display out))'")))) + marionette))) + + (test-end)))) + + (gexp->derivation "rootless-podman-oci-service-test" test)) + +(define %test-oci-service-rootless-podman + (system-test + (name "oci-service-rootless-podman") + (description "Test Rootless-Podman backed OCI provisioning service.") + (value (run-rootless-podman-oci-service-test)))) + +(define (%oci-docker-run commands) + #~((use-modules (srfi srfi-1) + (ice-9 format) + (ice-9 popen) + (ice-9 match) + (ice-9 rdelim) + (gnu build oci-containers)) + + #$%oci-read-lines + #$%oci-slurp + + (let ((outputs (list #$@commands))) + (map + (lambda (output) + (sort output string<=?)) + outputs)))) + +(define %oci-docker-os + (simple-operating-system + (service dhcp-client-service-type) + (service dbus-root-service-type) + (service polkit-service-type) + (service elogind-service-type) + (service containerd-service-type) + (service docker-service-type) + (extra-special-file "/shared.txt" + (plain-file "shared.txt" "hello")) + (service oci-service-type + (oci-configuration + (verbose? #t))) + (simple-service 'oci-provisioning + oci-service-type + (oci-extension + (networks + (list %oci-network)) + (volumes + (list %oci-volume)) + (containers + (list + (oci-container-configuration + (provision "first") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(use-modules (web server)) +(define (handler request request-body) + (values '((content-type . (text/plain))) \"out of office\")) +(run-server handler 'http `(#:addr ,(inet-pton AF_INET \"0.0.0.0\")))")) + (host-environment + '(("VARIABLE" . "value"))) + (volumes + '(("my-volume" . "/my-volume"))) + (extra-arguments + '("--env" "VARIABLE"))) + (oci-container-configuration + (provision "second") + (image + (oci-image + (repository "guile") + (value + (specifications->manifest '("guile"))) + (pack-options + '(#:symlinks (("/bin" -> "bin")))))) + (entrypoint "/bin/guile") + (network "my-network") + (command + '("-c" "(let l ((c 300)) +(display c) +(newline) +(sleep 1) +(when (positive? c) + (l (- c 1))))")) + (volumes + '(("my-volume" . "/my-volume") + ("/shared.txt" . "/shared.txt:ro")))))))))) + +(define (run-docker-oci-service-test) + (define os + (marionette-operating-system + (operating-system-with-gc-roots + %oci-docker-os + (list)) + #:imported-modules '((gnu build oci-containers) + (gnu build utils) + (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 oci-containers) + (gnu build utils) + (gnu build marionette)) + #~(begin + (use-modules (srfi srfi-1) (srfi srfi-11) (srfi srfi-64) + (gnu build utils) + (gnu build marionette)) + + (define marionette + ;; Relax timeout to accommodate older systems and + ;; allow for pulling the image. + (make-marionette (list #$vm) #:timeout 60)) + + (test-runner-current (system-test-runner #$output)) + (test-begin "docker-oci-service") + + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (wait-for-service 'dockerd)) + marionette) + + (test-assert "docker-volumes running" + (begin + (define (run-test) + (first + (marionette-eval + `(begin + #$@(%oci-docker-run + #~((oci-object-service-available? + "/run/current-system/profile/bin/docker" + "volume" + '("my-volume") + #:verbose? #t)))) + marionette))) + ;; Allow services to come up on slower machines. + (with-retries 80 1 (equal? '("my-volume") (run-test))))) + + (test-assert "docker-networks running" + (begin + (define (run-test) + (first + (marionette-eval + `(begin + #$@(%oci-docker-run + #~((oci-object-service-available? + "/run/current-system/profile/bin/docker" + "network" + '("my-network") + #:verbose? #t)))) + marionette))) + ;; Allow services to come up on slower machines. + (with-retries 80 1 (equal? + '("my-network" "none") + (run-test))))) + + (test-assert "passing host environment variables" + (begin + (define (run-test) + (first + (marionette-eval + `(begin + #$@(%oci-docker-run + #~((slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" + "'(display (getenv \"VARIABLE\"))'")))) + marionette))) + ;; Allow services to come up on slower machines. + (with-retries 80 1 (equal? '("value") (run-test))))) + + (test-equal "read and write to provisioned volumes" + '("world") + (second + (marionette-eval + `(begin + #$@(%oci-docker-run + #~((slurp + "/run/current-system/profile/bin/docker" + "exec" "first" + "/bin/guile" "-c" "'(begin +(use-modules (ice-9 popen) (ice-9 rdelim)) +(call-with-output-file \"/my-volume/out.txt\" + (lambda (p) (display \"world\" p))))'") + (slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "'(begin +(use-modules (ice-9 popen) (ice-9 rdelim)) +(display + (call-with-input-file \"/my-volume/out.txt\" read-line)))'")))) + marionette))) + + (test-equal + "can read and write to ports over provisioned network" + '("out of office") + (first + (marionette-eval + `(begin + #$@(%oci-docker-run + #~((slurp + "/run/current-system/profile/bin/docker" + "exec" "second" + "/bin/guile" "-c" "'(begin (use-modules (web client)) + (define-values (response out) + (http-get \"http://first:8080\")) + (display out))'")))) + marionette))) + + (test-end)))) + + (gexp->derivation "docker-oci-service-test" test)) + +(define %test-oci-service-docker + (system-test + (name "oci-service-docker") + (description "Test Docker backed OCI provisioning service.") + (value (run-docker-oci-service-test)))) -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 02:53:06 GMT) Full text and rfc822 format available.Message #229 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v11 4/5] tests: Use lower-oci-image-state in container tests. Date: Sun, 1 Jun 2025 04:51:17 +0200
This patch replaces boilerplate in container related tests with oci-image plumbing from (gnu services containers). * gnu/tests/containers.scm (%oci-tarball): New variable; (run-rootless-podman-test): use %oci-tarball; (build-tarball&run-rootless-podman-test): drop procedure. * gnu/tests/docker.scm (%docker-tarball): New variable; (build-tarball&run-docker-test): use %docker-tarball; (%docker-system-tarball): New variable; (build-tarball&run-docker-system-test): new procedure. Change-Id: Iad6f0704aee188d89464c83722dea0bb7adb084a Signed-off-by: Giacomo Leidi <goodoldpaul <at> autistici.org> --- gnu/tests/containers.scm | 84 +++++++++++++++---------------- gnu/tests/docker.scm | 104 ++++++++++++++++++++++----------------- 2 files changed, 101 insertions(+), 87 deletions(-) diff --git a/gnu/tests/containers.scm b/gnu/tests/containers.scm index 051a4c740bf..0db22e3a07f 100644 --- a/gnu/tests/containers.scm +++ b/gnu/tests/containers.scm @@ -46,6 +46,9 @@ (define-module (gnu tests containers) %test-oci-service-rootless-podman %test-oci-service-docker)) +(define lower-oci-image-state + (@@ (gnu services containers) lower-oci-image-state)) + (define %rootless-podman-os (simple-operating-system @@ -69,13 +72,48 @@ (define %rootless-podman-os (supplementary-groups '("wheel" "netdev" "cgroup" "audio" "video"))))))) -(define (run-rootless-podman-test oci-tarball) +(define %oci-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (package + (name "guest-script") + (version "0") + (source #f) + (build-system trivial-build-system) + (arguments + (list + #:guile guile-3.0 + #:builder + #~(let ((out #$output)) + (mkdir out) + (call-with-output-file (string-append out "/a.scm") + (lambda (port) + (display "(display \"hello world\n\")" port)))))) + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + +(define (run-rootless-podman-test) (define os (marionette-operating-system (operating-system-with-gc-roots %rootless-podman-os - (list oci-tarball)) + (list %oci-tarball)) #:imported-modules '((gnu services herd) (guix combinators)))) @@ -254,7 +292,7 @@ (define (run-rootless-podman-test oci-tarball) (let* ((loaded (slurp ,(string-append #$podman "/bin/podman") "load" "-i" - ,#$oci-tarball)) + ,#$%oci-tarball)) (repository&tag "localhost/guile-guest:latest") (response1 (slurp ,(string-append #$podman "/bin/podman") @@ -307,49 +345,11 @@ (define (run-rootless-podman-test oci-tarball) (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)))) + (value (run-rootless-podman-test)))) (define %oci-network diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 5dcf05a17e3..1f264d52486 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -26,6 +26,7 @@ (define-module (gnu tests docker) #:use-module (gnu system image) #:use-module (gnu system vm) #:use-module (gnu services) + #:use-module (gnu services containers) #:use-module (gnu services dbus) #:use-module (gnu services networking) #:use-module (gnu services docker) @@ -48,6 +49,9 @@ (define-module (gnu tests docker) %test-docker-system %test-oci-container)) +(define lower-oci-image-state + (@@ (gnu services containers) lower-oci-image-state)) + (define %docker-os (simple-operating-system (service dhcp-client-service-type) @@ -57,6 +61,41 @@ (define %docker-os (service containerd-service-type) (service docker-service-type))) +(define %docker-tarball + (lower-oci-image-state + "guile-guest" + (packages->manifest + (list + guile-3.0 guile-json-3 + (package + (name "guest-script") + (version "0") + (source #f) + (build-system trivial-build-system) + (arguments + (list + #:guile guile-3.0 + #:builder + #~(let ((out #$output)) + (mkdir out) + (call-with-output-file (string-append out "/a.scm") + (lambda (port) + (display "(display \"hello world\n\")" port)))))) + (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)))) + '(#:entry-point "bin/guile" + #:localstatedir? #t + #:extra-options (#:image-tag "guile-guest") + #:symlinks (("/bin/Guile" -> "bin/guile") + ("aa.scm" -> "a.scm"))) + "guile-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-test docker-tarball) "Load DOCKER-TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." @@ -173,40 +212,7 @@ (define (run-docker-test docker-tarball) (gexp->derivation "docker-test" test)) (define (build-tarball&run-docker-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")) - #:entry-point "bin/guile" - #:localstatedir? #t))) - (run-docker-test tarball))) + (run-docker-test %docker-tarball)) (define %test-docker (system-test @@ -215,8 +221,22 @@ (define %test-docker (value (build-tarball&run-docker-test)))) +(define %docker-system-tarball + (lower-oci-image-state + "guix-system-guest" + (operating-system + (inherit (simple-operating-system)) + ;; Use locales for a single libc to + ;; reduce space requirements. + (locale-libcs (list glibc))) + '() + "guix-system-guest" + (%current-target-system) + (%current-system) + #f)) + (define (run-docker-system-test tarball) - "Load DOCKER-TARBALL as Docker image and run it in a Docker container, + "Load TARBALL as Docker image and run it in a Docker container, inside %DOCKER-OS." (define os (marionette-operating-system @@ -333,21 +353,15 @@ (define (run-docker-system-test tarball) (gexp->derivation "docker-system-test" test)) +(define (build-tarball&run-docker-system-test) + (run-docker-system-test %docker-system-tarball)) + (define %test-docker-system (system-test (name "docker-system") (description "Run a system image as produced by @command{guix system docker-image} inside Docker.") - (value (with-monad %store-monad - (>>= (lower-object - (system-image (os->image - (operating-system - (inherit (simple-operating-system)) - ;; Use locales for a single libc to - ;; reduce space requirements. - (locale-libcs (list glibc))) - #:type docker-image-type))) - run-docker-system-test))))) + (value (build-tarball&run-docker-system-test)))) (define %oci-os -- 2.49.0
guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 02:53:09 GMT) Full text and rfc822 format available.Message #232 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v11 1/5] tests: oci-container: Set explicit timeouts. Date: Sun, 1 Jun 2025 04:51:14 +0200
* gnu/tests/docker.scm: Simplify %test-oci-container test case and add explicit timeouts to tests outcomes. Signed-off-by: Giacomo Leidi <goodoldpaul <at> autistici.org> --- gnu/tests/docker.scm | 99 ++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/gnu/tests/docker.scm b/gnu/tests/docker.scm index 90c8d0f8508..5dcf05a17e3 100644 --- a/gnu/tests/docker.scm +++ b/gnu/tests/docker.scm @@ -1,7 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2019 Danny Milosavljevic <dannym <at> scratchpost.org> ;;; Copyright © 2019-2023 Ludovic Courtès <ludo <at> gnu.org> -;;; Copyright © 2024 Giacomo Leidi <goodoldpaul <at> autistici.org> +;;; Copyright © 2024, 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -414,71 +414,54 @@ (define (run-oci-container-test) (test-runner-current (system-test-runner #$output)) (test-begin "oci-container") - (test-assert "containerd service running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'containerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (test-assert "containerd PID file present" - (wait-for-file "/run/containerd/containerd.pid" marionette)) - - (test-assert "dockerd running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'dockerd) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - marionette)) - - (sleep 10) ; let service start + (wait-for-file "/run/containerd/containerd.pid" marionette) (test-assert "docker-guile running" (marionette-eval '(begin (use-modules (gnu services herd)) - (match (start-service 'docker-guile) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) + (wait-for-service 'docker-guile #:timeout 120) + #t) marionette)) - (test-equal "passing host environment variables and volumes" - '("value" "hello") - (marionette-eval - `(begin - (use-modules (ice-9 popen) - (ice-9 rdelim)) - - (define slurp - (lambda args - (let* ((port (apply open-pipe* OPEN_READ args)) - (output (let ((line (read-line port))) - (if (eof-object? line) - "" - line))) - (status (close-pipe port))) - output))) - (let* ((response1 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) - (response2 (slurp - ,(string-append #$docker-cli "/bin/docker") - "exec" "docker-guile" - "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) + (test-assert "passing host environment variables and volumes" + (begin + (define (run-test) + (marionette-eval + `(begin + (use-modules (ice-9 popen) + (ice-9 rdelim)) + + (define slurp + (lambda args + (let* ((port (apply open-pipe* OPEN_READ args)) + (output (let ((line (read-line port))) + (if (eof-object? line) + "" + line))) + (status (close-pipe port))) + output))) + (let* ((response1 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(display (getenv \"VARIABLE\"))")) + (response2 (slurp + ,(string-append #$docker-cli "/bin/docker") + "exec" "docker-guile" + "/bin/guile" "-c" "(begin (use-modules (ice-9 popen) (ice-9 rdelim)) (display (call-with-input-file \"/shared.txt\" read-line)))"))) - (list response1 response2))) - marionette)) + (list response1 response2))) + marionette)) + ;; Allow services to come up on slower machines + (let loop ((attempts 0)) + (if (= attempts 60) + (error "Service didn't come up after more than 60 seconds") + (if (equal? '("value" "hello") + (run-test)) + #t + (begin + (sleep 1) + (loop (+ 1 attempts)))))))) (test-end)))) base-commit: e29c57ab81517424b03579147910553d92246212 -- 2.49.0
maxim.cournoyer <at> gmail.com, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 02:53:12 GMT) Full text and rfc822 format available.Message #235 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v11 2/5] gnu: Move with-retries outside dbus-service. Date: Sun, 1 Jun 2025 04:51:15 +0200
This patch moves with-retries outside of (gnu build dbus-service) into a more general (gnu build utils) which can be imported without unnecessarily importing dbus related symbols. * gnu/build/dbus-service.scm (sleep,with-retries): Move to... * gnu/build/utils.scm: ...here. * gnu/local.mk: Add gnu/build/utils.scm. * gnu/build/jami-service.scm: Import (gnu build utils). * gnu/services/telephony.scm (jami-account->alist): Format. (jami-shepherd-services): Import (gnu build utils). * gnu/test/messaging.scm (run-ngircd-test): Import (gnu build utils). (run-pounce-test): Import (gnu build utils). * gnu/test/telephony.scm (run-jami-test): Import (gnu build utils) and format. Change-Id: I3c1768f884ca46d0820a801bd0310c2ec8f3da54 --- gnu/build/dbus-service.scm | 39 +++------------------------ gnu/build/jami-service.scm | 1 + gnu/build/utils.scm | 55 ++++++++++++++++++++++++++++++++++++++ gnu/local.mk | 1 + gnu/services/telephony.scm | 9 ++++--- gnu/tests/messaging.scm | 8 +++--- gnu/tests/telephony.scm | 11 ++++---- 7 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 gnu/build/utils.scm diff --git a/gnu/build/dbus-service.scm b/gnu/build/dbus-service.scm index 688afe44c3d..9bbcd457512 100644 --- a/gnu/build/dbus-service.scm +++ b/gnu/build/dbus-service.scm @@ -1,5 +1,6 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2021, 2022 Maxim Cournoyer <maxim.cournoyer <at> gmail.com> +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> autistici.org> ;;; ;;; This file is part of GNU Guix. ;;; @@ -24,6 +25,7 @@ ;;; Code: (define-module (gnu build dbus-service) + #:use-module (gnu build utils) #:use-module (ice-9 match) #:use-module (srfi srfi-1) #:use-module (srfi srfi-19) @@ -54,45 +56,10 @@ (define-module (gnu build dbus-service) call-dbus-method dbus-available-services - dbus-service-available? - - with-retries)) + dbus-service-available?)) (define %dbus-query-timeout 2) ;in seconds -;;; Use Fibers' sleep to enable cooperative scheduling in Shepherd >= 0.9.0, -;;; which is required at least for the Jami service. -(define sleep* - (lambda () ;delay execution - (if (resolve-module '(fibers) #f #:ensure #f) - (module-ref (resolve-interface '(fibers)) 'sleep) - (begin - (format #t "Fibers not available -- blocking 'sleep' in use~%") - sleep)))) - -;;; -;;; Utilities. -;;; - -(define-syntax-rule (with-retries n delay body ...) - "Retry the code in BODY up to N times until it doesn't raise an exception nor -return #f, else raise an error. A delay of DELAY seconds is inserted before -each retry." - (let loop ((attempts 0)) - (catch #t - (lambda () - (let ((result (begin body ...))) - (if (not result) - (error "failed attempt" attempts) - result))) - (lambda args - (if (< attempts n) - (begin - ((sleep*) delay) ;else wait and retry - (loop (+ 1 attempts))) - (error "maximum number of retry attempts reached" - (quote body ...) args)))))) - ;;; ;;; Low level wrappers above AC/D-Bus. diff --git a/gnu/build/jami-service.scm b/gnu/build/jami-service.scm index a00785f699b..7c2c48d821a 100644 --- a/gnu/build/jami-service.scm +++ b/gnu/build/jami-service.scm @@ -25,6 +25,7 @@ (define-module (gnu build jami-service) #:use-module (gnu build dbus-service) + #:use-module (gnu build utils) #:use-module (ice-9 format) #:use-module (ice-9 match) #:use-module (ice-9 rdelim) diff --git a/gnu/build/utils.scm b/gnu/build/utils.scm new file mode 100644 index 00000000000..1aa72358bd8 --- /dev/null +++ b/gnu/build/utils.scm @@ -0,0 +1,55 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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/>. + +;;; Commentary: +;;; +;;; This module contains helpers that could useful to any service. +;;; +;;; Code: + +(define-module (gnu build utils) + #:export (with-retries)) + +;;; Use Fibers' sleep to enable cooperative scheduling in Shepherd >= 0.9.0, +;;; which is required at least for the Jami service. +(define sleep* + (lambda () ;delay execution + (if (resolve-module '(fibers) #f #:ensure #f) + (module-ref (resolve-interface '(fibers)) 'sleep) + (begin + (format #t "Fibers not available -- blocking 'sleep' in use~%") + sleep)))) + +(define-syntax-rule (with-retries n delay body ...) + "Retry the code in BODY up to N times until it doesn't raise an exception nor +return #f, else raise an error. A delay of DELAY seconds is inserted before +each retry." + (let loop ((attempts 0)) + (catch #t + (lambda () + (let ((result (begin body ...))) + (if (not result) + (error "failed attempt" attempts) + result))) + (lambda args + (if (< attempts n) + (begin + ((sleep*) delay) ;else wait and retry + (loop (+ 1 attempts))) + (error "maximum number of retry attempts reached" + (quote body ...) args)))))) diff --git a/gnu/local.mk b/gnu/local.mk index 2948bfb1bff..4500bfc6d33 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -841,6 +841,7 @@ GNU_SYSTEM_MODULES = \ %D%/build/linux-modules.scm \ %D%/build/marionette.scm \ %D%/build/secret-service.scm \ + %D%/build/utils.scm \ \ %D%/tests.scm \ %D%/tests/audio.scm \ diff --git a/gnu/services/telephony.scm b/gnu/services/telephony.scm index 9926f4107de..ad6959e161b 100644 --- a/gnu/services/telephony.scm +++ b/gnu/services/telephony.scm @@ -210,7 +210,7 @@ (define (jami-account->alist jami-account-object) (tfilter-maybe-value jami-account-object) (tmap (lambda (field) (let* ((name (field-name->account-detail - (configuration-field-name field))) + (configuration-field-name field))) (value ((configuration-field-serializer field) name ((configuration-field-getter field) jami-account-object)))) @@ -360,7 +360,8 @@ (define (jami-shepherd-services config) ;; variant of the 'sleep' procedure. guile-fibers) (with-imported-modules (source-module-closure - '((gnu build dbus-service) + '((gnu build utils) + (gnu build dbus-service) (gnu build jami-service) (gnu system file-systems))) @@ -541,7 +542,8 @@ (define (jami-shepherd-services config) (list (shepherd-service (documentation "Run a D-Bus session for the Jami daemon.") (provision '(jami-dbus-session)) - (modules `((gnu build dbus-service) + (modules `((gnu build utils) + (gnu build dbus-service) (gnu build jami-service) (gnu system file-systems) ,@%default-modules)) @@ -587,6 +589,7 @@ (define (jami-shepherd-services config) (ice-9 receive) (srfi srfi-1) (srfi srfi-26) + (gnu build utils) (gnu build dbus-service) (gnu build jami-service) (gnu system file-systems) diff --git a/gnu/tests/messaging.scm b/gnu/tests/messaging.scm index 8df67433a7f..2eb99331b52 100644 --- a/gnu/tests/messaging.scm +++ b/gnu/tests/messaging.scm @@ -269,7 +269,7 @@ (define (run-ngircd-test) (marionette-operating-system %ngircd-os #:imported-modules (source-module-closure - '((gnu build dbus-service) + '((gnu build utils) (guix build utils) (gnu services herd))))))) @@ -298,7 +298,7 @@ (define (run-ngircd-test) (test-assert "basic irc operations function as expected" (marionette-eval '(begin - (use-modules ((gnu build dbus-service) #:select (with-retries)) + (use-modules (gnu build utils) (ice-9 textual-ports)) (define (write-command command) @@ -437,7 +437,7 @@ (define (run-pounce-test) (marionette-operating-system %pounce-os #:imported-modules (source-module-closure - '((gnu build dbus-service) + '((gnu build utils) (guix build utils) (gnu services herd))))) (memory-size 1024))) @@ -470,7 +470,7 @@ (define (run-pounce-test) (test-assert "pounce functions as an irc bouncer" (marionette-eval '(begin - (use-modules ((gnu build dbus-service) #:select (with-retries)) + (use-modules (gnu build utils) (guix build utils) (ice-9 textual-ports)) diff --git a/gnu/tests/telephony.scm b/gnu/tests/telephony.scm index f03ea963f7e..3a085762323 100644 --- a/gnu/tests/telephony.scm +++ b/gnu/tests/telephony.scm @@ -143,7 +143,8 @@ (define* (run-jami-test #:key provisioning? partial?) #:imported-modules '((gnu services herd) (guix combinators) (gnu build jami-service) - (gnu build dbus-service)))) + (gnu build dbus-service) + (gnu build utils)))) (define vm (virtual-machine (operating-system os) (memory-size 512))) @@ -209,7 +210,7 @@ (define* (run-jami-test #:key provisioning? partial?) (test-assert "service can be stopped" (marionette-eval '(begin - (use-modules (gnu build dbus-service) + (use-modules (gnu build utils) (gnu build jami-service) (gnu services herd) (rnrs base)) @@ -223,10 +224,10 @@ (define* (run-jami-test #:key provisioning? partial?) (test-assert "service can be restarted" (marionette-eval '(begin - (use-modules (gnu build dbus-service) + (use-modules (gnu build utils) (gnu build jami-service) (gnu services herd) - (rnrs base) ) + (rnrs base)) ;; Start the service. (start-service 'jami) (with-retries 40 1 (jami-service-available?)) @@ -239,7 +240,7 @@ (define* (run-jami-test #:key provisioning? partial?) (test-assert "jami accounts provisioning, account present" (marionette-eval '(begin - (use-modules (gnu build dbus-service) + (use-modules (gnu build utils) (gnu services herd) (rnrs base)) ;; Accounts take some time to appear after being added. -- 2.49.0
andrew <at> trop.in, gabriel <at> erlikon.ch, hako <at> ultrarare.space, janneke <at> gnu.org, ludo <at> gnu.org, maxim.cournoyer <at> gmail.com, tanguy <at> bioneland.org, guix-patches <at> gnu.org
:bug#76081
; Package guix-patches
.
(Sun, 01 Jun 2025 02:53:16 GMT) Full text and rfc822 format available.Message #238 received at 76081 <at> debbugs.gnu.org (full text, mbox):
From: Giacomo Leidi <goodoldpaul <at> autistici.org> To: 76081 <at> debbugs.gnu.org Cc: Giacomo Leidi <goodoldpaul <at> autistici.org> Subject: [PATCH v11 5/5] home: Add home-oci-service-type. Date: Sun, 1 Jun 2025 04:51:18 +0200
* gnu/home/service/containers.scm: New file; * gnu/local.mk (GNU_SYSTEM_MODULES): Add it. * doc/guix.texi (OCI backed services): Document it. Change-Id: I8ce5b301e8032d0a7b2a9ca46752738cdee1f030 Signed-off-by: Giacomo Leidi <goodoldpaul <at> autistici.org> --- doc/guix.texi | 114 +++++++++++++++++++++++++++++++ gnu/home/services/containers.scm | 49 +++++++++++++ gnu/local.mk | 1 + 3 files changed, 164 insertions(+) create mode 100644 gnu/home/services/containers.scm diff --git a/doc/guix.texi b/doc/guix.texi index ae49d0c547b..2821712e8b4 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -52192,6 +52192,120 @@ Miscellaneous Home Services documentation of the system service (@pxref{Miscellaneous Services, @code{readymedia-service-type}}). +@subsubheading OCI backed services + +@cindex OCI-backed, for Home +The @code{(gnu home services containers)} module provides the following service: + +@defvar home-oci-service-type +This is the type of the service that allows to manage your OCI containers with +the same consistent interface you use for your other Home Shepherd services. +@end defvar + +This service is a direct mapping of the @code{oci-service-type} system +service (@pxref{Miscellaneous Services, OCI backed services}). You can +use it like this: + +@lisp +(use-modules (gnu services containers) + (gnu home services containers)) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) + +@end lisp + +You may specify a custom configuration by providing a +@code{oci-configuration} record, exactly like for +@code{oci-service-type}, but wrapping it in @code{for-home}: + +@lisp +(use-modules (gnu services) + (gnu services containers) + (gnu home services containers)) + +(service home-oci-service-type + (for-home + (oci-configuration + (runtime 'podman) + (verbose? #t)))) + +(simple-service 'home-oci-provisioning + home-oci-service-type + (oci-extension + (volumes + (list + (oci-volume-configuration (name "prometheus")) + (oci-volume-configuration (name "grafana")))) + (networks + (list + (oci-network-configuration (name "monitoring")))) + (containers + (list + (oci-container-configuration + (network "monitoring") + (image + (oci-image + (repository "guile") + (tag "3") + (value (specifications->manifest '("guile"))) + (pack-options '(#:symlinks (("/bin/guile" -> "bin/guile")) + #:max-layers 2)))) + (entrypoint "/bin/guile") + (command + '("-c" "(display \"hello!\n\")"))) + (oci-container-configuration + (image "prom/prometheus") + (network "monitoring") + (ports + '(("9000" . "9000") + ("9090" . "9090"))) + (volumes + (list + '(("prometheus" . "/var/lib/prometheus"))))) + (oci-container-configuration + (image "grafana/grafana:10.0.1") + (network "monitoring") + (volumes + '(("grafana:/var/lib/grafana")))))))) +@end lisp + @node Invoking guix home @section Invoking @command{guix home} diff --git a/gnu/home/services/containers.scm b/gnu/home/services/containers.scm new file mode 100644 index 00000000000..1ccdb3b2464 --- /dev/null +++ b/gnu/home/services/containers.scm @@ -0,0 +1,49 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Giacomo Leidi <goodoldpaul <at> 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 home services containers) + #:use-module (gnu home services) + #:use-module (gnu home services shepherd) + #:use-module (gnu services) + #:use-module (gnu services configuration) + #:use-module (gnu services containers) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (srfi srfi-1) + #:export (home-oci-service-type)) + +(define home-oci-service-type + (service-type + (inherit (system->home-service-type oci-service-type)) + (extensions + (list + (service-extension home-profile-service-type + (lambda (config) + (let ((runtime-cli + (oci-configuration-runtime-cli config)) + (runtime + (oci-configuration-runtime config))) + (oci-service-profile runtime runtime-cli)))) + (service-extension home-shepherd-service-type + oci-configuration->shepherd-services))) + (extend + (lambda (config extension) + (for-home + (oci-configuration + (inherit (oci-configuration-extend config extension)))))) + (default-value (for-home (oci-configuration))))) diff --git a/gnu/local.mk b/gnu/local.mk index 09ccd64b40a..5dfc20da9ca 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -104,6 +104,7 @@ GNU_SYSTEM_MODULES = \ %D%/home/services.scm \ %D%/home/services/admin.scm \ %D%/home/services/backup.scm \ + %D%/home/services/containers.scm \ %D%/home/services/desktop.scm \ %D%/home/services/dict.scm \ %D%/home/services/dotfiles.scm \ -- 2.49.0
GNU bug tracking system
Copyright (C) 1999 Darren O. Benham,
1997,2003 nCipher Corporation Ltd,
1994-97 Ian Jackson.