;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2024, 2025 Giacomo Leidi <therewasa@fishinthecalculator.me>
;;; Copyright © 2025 Owen T. Heisler <writer@owenh.net>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu services containers)
  #:use-module (gnu 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 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?
            rootless-podman-configuration-fields
            rootless-podman-configuration-podman
            rootless-podman-configuration-group-name
            rootless-podman-configuration-containers-registries
            rootless-podman-configuration-containers-storage
            rootless-podman-configuration-containers-policy
            rootless-podman-configuration-pam-limits
            rootless-podman-configuration-subgids
            rootless-podman-configuration-subuids

            rootless-podman-service-subids
            rootless-podman-service-accounts
            rootless-podman-service-profile
            rootless-podman-shepherd-services
            rootless-podman-service-etc

            rootless-podman-service-type

            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

            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-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)
      (string? value)))

(define (lowerable? value)
  (or (file-like? value)
      (gexp-or-string? value)))

(define list-of-pam-limits-entries?
  (list-of pam-limits-entry?))

(define list-of-subid-ranges?
  (list-of subid-range?))

(define (package-or-#f? val)
  (or (not val)
      (package? val)))

(define-configuration/no-serialization rootless-podman-configuration
  (podman
   (package-or-#f podman)
   "The Podman package that will be installed in the system profile.
@code{#f} can be passed to suppress the installation.")
  (group-name
   (string "cgroup")
   "The name of the group that will own /sys/fs/cgroup resources.  Users that
want to use rootless Podman have to be in this group.")
  (containers-registries
   (lowerable
    (plain-file "registries.conf"
                (string-append "unqualified-search-registries = ['docker.io','"
                               "registry.fedora.org','registry.opensuse.org']")))
   "A string or a gexp evaluating to the path of Podman's
@code{containers/registries.conf} configuration file.")
  (containers-storage
   (lowerable
    (plain-file "storage.conf"
                "[storage]
driver = \"overlay\""))
   "A string or a gexp evaluating to the path of Podman's
@code{containers/storage.conf} configuration file.")
  (containers-policy
   (lowerable
    (plain-file "policy.json"
                "{\"default\": [{\"type\": \"insecureAcceptAnything\"}]}"))
   "A string or a gexp evaluating to the path of Podman's
@code{containers/policy.json} configuration file.")
  (pam-limits
   (list-of-pam-limits-entries
    (list (pam-limits-entry "*" 'both 'nofile 100000)))
   "The PAM limits to be set for rootless Podman.")
  (subgids
   (list-of-subid-ranges '())
   "A list of subid ranges representing the subgids that will be
available for each configured user.")
  (subuids
   (list-of-subid-ranges '())
   "A list of subid ranges representing the subuids that will be
available for each configured user."))

(define rootless-podman-service-profile
  (lambda (config)
    (or (and=> (rootless-podman-configuration-podman config) list)
        (list))))

(define rootless-podman-service-etc
  (lambda (config)
    (list `("containers/registries.conf"
            ,(rootless-podman-configuration-containers-registries config))
          `("containers/storage.conf"
            ,(rootless-podman-configuration-containers-storage config))
          `("containers/policy.json"
            ,(rootless-podman-configuration-containers-policy config)))))

(define rootless-podman-service-subids
  (lambda (config)
    (subids-extension
     (subgids (rootless-podman-configuration-subgids config))
     (subuids (rootless-podman-configuration-subuids config)))))

(define rootless-podman-service-accounts
  (lambda (config)
    (list (user-group (name (rootless-podman-configuration-group-name config))
                      (system? #t)))))

(define (cgroups-fs-owner-entrypoint config)
  (define group
    (rootless-podman-configuration-group-name config))
  (program-file "cgroups2-fs-owner-entrypoint"
                #~(system*
                   (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 && "
                                  "chmod -v 775 /sys/fs/cgroup && chown -v "
                                  "root:" #$group " /sys/fs/cgroup/cgroup."
                                  "{procs,subtree_control,threads} && "
                                  "chmod -v 664 /sys/fs/cgroup/cgroup."
                                  "{procs,subtree_control,threads}"))))

(define (rootless-podman-cgroups-fs-owner-service config)
  (shepherd-service (provision '(cgroups2-fs-owner))
                    (requirement
                     '(user-processes
                       dbus-system
                       elogind
                       file-system-/sys/fs/cgroup
                       networking
                       udev
                       cgroups2-limits))
                    (one-shot? #t)
                    (documentation
                     "Set ownership of /sys/fs/cgroup to the configured group.")
                    (start
                     #~(make-forkexec-constructor
                        (list
                         #$(cgroups-fs-owner-entrypoint config))))
                    (stop
                     #~(make-kill-destructor))))

(define cgroups-limits-entrypoint
  (program-file "cgroups2-limits-entrypoint"
                #~(system*
                   (string-append #+bash-minimal "/bin/bash") "-c"
                   (string-append "echo Setting cgroups v2 limits && "
                                  "echo +cpu +cpuset +io +memory +pids"
                                  " >> /sys/fs/cgroup/cgroup.subtree_control"))))

(define (rootless-podman-cgroups-limits-service config)
  (shepherd-service (provision '(cgroups2-limits))
                    (requirement
                     '(user-processes
                       dbus-system
                       elogind
                       networking
                       udev
                       file-system-/sys/fs/cgroup
                       rootless-podman-shared-root-fs))
                    (one-shot? #t)
                    (documentation
                     "Allow setting cgroups limits: cpu, cpuset, io, memory and
pids.")
                    (start
                     #~(make-forkexec-constructor
                        (list
                         #$cgroups-limits-entrypoint)))
                    (stop
                     #~(make-kill-destructor))))

(define rootless-podman-shared-root-fs-entrypoint
  (program-file "rootless-podman-shared-root-fs-entrypoint"
                #~(system*
                   "/run/privileged/bin/mount" "--make-shared" "/")))

(define (rootless-podman-shared-root-fs-service config)
  (shepherd-service (provision '(rootless-podman-shared-root-fs))
                    (requirement
                     '(user-processes))
                    (one-shot? #t)
                    (documentation
                     "Buildah/Podman running as rootless expects the bind mount
to be shared.  This service sets it so.")
                    (start
                     #~(make-forkexec-constructor
                        (list
                         #$rootless-podman-shared-root-fs-entrypoint)))
                    (stop
                     #~(make-kill-destructor))))

(define (rootless-podman-shepherd-services config)
  (list
   (rootless-podman-shared-root-fs-service config)
   (rootless-podman-cgroups-limits-service config)
   (rootless-podman-cgroups-fs-owner-service config)))

(define rootless-podman-service-type
  (service-type (name 'rootless-podman)
                (extensions
                 (list
                  (service-extension subids-service-type
                                     rootless-podman-service-subids)
                  (service-extension account-service-type
                                     rootless-podman-service-accounts)
                  (service-extension profile-service-type
                                     rootless-podman-service-profile)
                  (service-extension shepherd-root-service-type
                                     rootless-podman-shepherd-services)
                  (service-extension pam-limits-service-type
                                     rootless-podman-configuration-pam-limits)
                  (service-extension etc-service-type
                                     rootless-podman-service-etc)))
                (default-value (rootless-podman-configuration))
                (description
                 "This service configures rootless @code{podman} on the Guix System.")))


;;;
;;; 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)
        (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-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)
     (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-container-shepherd-actions? value)
  (list? (oci-sanitize-shepherd-actions 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-object-extra-arguments? value)
  (list? (oci-sanitize-extra-arguments value)))

(define (oci-image-reference 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 (string-or-list-of-strings? value)
  (if (or (string? value)
          ((list-of string) value))
      value
      (raise
       (formatted-message
        (G_ "entrypoint may only be a single string or a list of strings
but ~a was found") value))))

(define (oci-lowerable-image? image)
  (or (manifest? image)
      (operating-system? image)
      (file-like? image)))

(define (string-or-oci-image? image)
  (or (string? image)
      (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 string-or-list-of-strings)
(define-maybe/no-serialization package)
(define-maybe/no-serialization subid-range)

(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
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
   (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
   (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.")
  (entrypoint
   (maybe-string-or-list-of-strings)
   "Overwrite the default entrypoint (@code{ENTRYPOINT}) of the image.  It can
be either a string, or a list of strings.")
  (host-environment
   (oci-container-host-environment '())
   "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\")
      \"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
   (oci-container-environment '())
   "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 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 OCI runtime,
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
   (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 an OCI network for the spawned container.")
  (ports
   (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:

@lisp
(list '(\"8080\" . \"80\")
      \"10443:443\")
@end lisp

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."
   (sanitizer oci-sanitize-ports))
  (volumes
   (oci-container-volumes '())
   "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 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,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 directory for the spawned Shepherd service.
You can refer to the
@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
   (oci-object-extra-arguments '())
   "A list of strings, gexps or file-like objects that will be directly passed
to the @command{docker run} or @command{podman run} invocation."
   (sanitizer oci-sanitize-extra-arguments)))

(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?)
                   (list (if (maybe-value-set? entrypoint)
                             `("--entrypoint"
                               ,(if (list? entrypoint)
                                    (string-join 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?)
                   (list (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
         (system-image (os->image os #:type docker-image-type))
         system
         #:target target)))
    (return tarball)))

(define (lower-manifest value options image-reference
                        target system grafts?)
  "Lower VALUE, a manifest record, into a tarball containing an OCI image."
  (define (format reference)
    ;; Remove from REFERENCE characters that cannot be used in the store.
    (string-map (lambda (chr)
                  (if (and (char-set-contains? char-set:ascii chr)
                           (char-set-contains? char-set:graphic chr)
                           (not (memv chr '(#\. #\/ #\: #\space))))
                      chr
                      #\-))
                reference))
  (mlet* %store-monad
      ((_ (set-grafting grafts?))
       (guile (set-guile-for-build (default-guile)))
       (profile
         (profile-derivation value
                             #:target target
                             #:system system
                             #:hooks '()
                             #:locales? #f))
       (tarball (apply pack:docker-image
                       `(,(format image-reference)
                         ,profile
                         ,@options
                         #:localstatedir? #t))))
    (return tarball)))

(define (lower-oci-image image)
  "Lower IMAGE, a oci-image record, into a tarball containing an OCI image."
  (match-record image <oci-image>
                (value pack-options target system grafts?)
    (define image-target
      (if (maybe-value-set? target)
          target
          (%current-target-system)))
    (define image-system
      (if (maybe-value-set? system)
          system
          (%current-system)))
    (match value
      ((? manifest? value)
       (lower-manifest value pack-options
                       (oci-image-reference image)
                       image-target image-system grafts?))
      ((? operating-system? value)
       (lower-operating-system value image-target image-system))
      ((? file-like? value)
       (lower-object value))
      (_
       (raise
        (formatted-message
         (G_ "oci-image value must contain only manifest,
operating-system, or file-like records but ~a was found")
         value))))))

(define-gexp-compiler (oci-image-compiler (image <oci-image>) system target)
  (lower-oci-image 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."
  (with-imported-modules (source-module-closure '((gnu build oci-containers)))
    (program-file
     (format #f "~a-image-loader" name)
     #~(begin
         (use-modules (gnu build oci-containers))
         (oci-image-load '#$(oci-runtime-state-runtime runtime-state)
                         #$(oci-runtime-state-runtime-cli runtime-state)
                         #$image #$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)
         ;; In case this specific container is not run through the user
         ;; provisioned by the oci-service-type, its environment is different
         ;; from the globally provisioned one and must be recomputed.
         (list #$@(if (maybe-value-set? user)
                      (oci-runtime-system-environment runtime user)
                      (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
                        (oci-runtime-name runtime)
                        " backed Shepherd service for "
                        (if (oci-image? image) shepherd-name image) "."))
                      (start
                       (container-action
                        (list start-entrypoint)))
                      (stop
                       (container-action
                        (list
                         (oci-container-entrypoint
                          shepherd-name (list runtime-cli "rm" "-f" shepherd-name)
                          #:verbose? verbose?))))
                      (actions
                       (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)."
                                       shepherd-name image))
                              (procedure
                               (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 user)
         (comment "OCI services account")
         (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))
  (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.")))
